diff --git a/.nf-core.yml b/.nf-core.yml index 1e590ce5..14dbff9c 100644 --- a/.nf-core.yml +++ b/.nf-core.yml @@ -1,8 +1,8 @@ lint: actions_ci: false files_exist: - - .github/workflows/awsfulltest.yml - - .github/workflows/awstest.yml + - conf/igenomes.config + - conf/igenomes_ignored.config files_unchanged: - .gitignore - assets/nf-core-spatialaxe_logo_light.png diff --git a/README.md b/README.md index bb47242c..df466808 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![Open in GitHub Codespaces](https://img.shields.io/badge/Open_In_GitHub_Codespaces-black?labelColor=grey&logo=github)](https://github.com/codespaces/new/nf-core/spatialaxe) [![GitHub Actions CI Status](https://github.com/nf-core/spatialaxe/actions/workflows/nf-test.yml/badge.svg)](https://github.com/nf-core/spatialaxe/actions/workflows/nf-test.yml) -[![GitHub Actions Linting Status](https://github.com/nf-core/spatialaxe/actions/workflows/linting.yml/badge.svg)](https://github.com/nf-core/spatialaxe/actions/workflows/linting.yml)[![AWS CI](https://img.shields.io/badge/CI%20tests-full%20size-FF9900?labelColor=000000&logo=Amazon%20AWS)](https://nf-co.re/spatialaxe/results)[![Cite with Zenodo](http://img.shields.io/badge/DOI-10.5281/zenodo.XXXXXXX-1073c8?labelColor=000000)](https://doi.org/10.5281/zenodo.XXXXXXX) +[![GitHub Actions Linting Status](https://github.com/nf-core/spatialaxe/actions/workflows/linting.yml/badge.svg)](https://github.com/nf-core/spatialaxe/actions/workflows/linting.yml)[![AWS CI](https://img.shields.io/badge/CI%20tests-full%20size-FF9900?labelColor=000000&logo=Amazon%20AWS)](https://nf-co.re/spatialaxe/results)[![Cite with Zenodo](http://img.shields.io/badge/DOI-10.5281/zenodo.20733817-1073c8?labelColor=000000)](https://doi.org/10.5281/zenodo.20733817) [![nf-test](https://img.shields.io/badge/unit_tests-nf--test-337ab7.svg)](https://www.nf-test.com) [![Nextflow](https://img.shields.io/badge/version-%E2%89%A525.04.0-green?style=flat&logo=nextflow&logoColor=white&color=%230DC09D&link=https%3A%2F%2Fnextflow.io)](https://www.nextflow.io/) @@ -181,7 +181,7 @@ For further information or help, don't hesitate to get in touch on the [Slack `# ## Citations - + An extensive list of references for the tools used by the pipeline can be found in the [`CITATIONS.md`](CITATIONS.md) file. diff --git a/assets/email_template.html b/assets/email_template.html index 636a526a..27657703 100644 --- a/assets/email_template.html +++ b/assets/email_template.html @@ -4,7 +4,7 @@ - + nf-core/spatialaxe Pipeline Report diff --git a/bin/baysor_create_dataset.py b/bin/baysor_create_dataset.py index 4e5a263a..2644caaa 100755 --- a/bin/baysor_create_dataset.py +++ b/bin/baysor_create_dataset.py @@ -46,11 +46,9 @@ def generate_dataset( reader = csv.reader(infile) writer = csv.writer(outfile) - # get the header line header = next(reader) writer.writerow(header) - # randomize csv rows to write for row in reader: if random.random() < float(sample_fraction): writer.writerow(row) @@ -59,9 +57,7 @@ def generate_dataset( def main() -> None: - """ - Run create dataset as nf module - """ + parser = argparse.ArgumentParser( description="Create sampled dataset for Baysor preview" ) @@ -79,11 +75,11 @@ def main() -> None: ) args = parser.parse_args() - sampled_transcripts = "sampled_transcripts.csv" + sampled_transcripts = Path("sampled_transcripts.csv") # generate dataset BaysorPreview.generate_dataset( - transcripts=args.transcripts, + transcripts=Path(args.transcripts), sampled_transcripts=sampled_transcripts, sample_fraction=args.sample_fraction, prefix=args.prefix diff --git a/bin/baysor_estimate_scale_factor.py b/bin/baysor_estimate_scale_factor.py new file mode 100755 index 00000000..36c5b578 --- /dev/null +++ b/bin/baysor_estimate_scale_factor.py @@ -0,0 +1,332 @@ +#!/usr/bin/env python3 + +""" +Estimate a recommended Baysor --scale parameter from Xenium transcripts. +""" + +import argparse +import math +from pathlib import Path + +import numpy as np # type: ignore +import pandas as pd # type: ignore +from numpy.typing import NDArray # type: ignore +from pandas import DataFrame, Series # type: ignore +from scipy.spatial import cKDTree # type: ignore + +from utils.utils_logger import logger + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Estimate Baysor --scale from Xenium transcript coordinates." + ) + + parser.add_argument( + "--transcripts", + required=True, + type=Path, + help="Path to transcripts file (.parquet or .csv)", + ) + parser.add_argument( + "--cell-column", + default=None, + help=( + "Column name for cell IDs. Omit when no prior segmentation is " + "available; the scale will instead be estimated from local " + "transcript density (k-nearest-neighbor distances)." + ), + ) + parser.add_argument("--prefix", default="", help="Prefix for output files") + parser.add_argument("--x-column", default="x_location", help="Column name for x coordinates") + parser.add_argument("--y-column", default="y_location", help="Column name for y coordinates") + + parser.add_argument("--percentile", type=float, default=90.0, help="Percentile for cell radius calculation") + parser.add_argument("--min-transcripts", type=int, default=10, help="Minimum number of transcripts per cell") + + parser.add_argument( + "--knn-neighbors", + type=int, + default=10, + help="Number of nearest neighbors to use for density-based radius estimation (no-prior case)", + ) + + parser.add_argument("--max-scale", type=float, default=30.0, help="Maximum scale value") + parser.add_argument("--min-scale", type=float, default=3.0, help="Minimum scale value") + parser.add_argument("--verbose", action="store_true", help="Enable verbose logging") + + return parser.parse_args() + + +def median_absolute_deviation(values: NDArray[np.floating]) -> float: + median: float = float(np.median(values)) + return float(np.median(np.abs(values - median))) + + +def trim_outliers_hybrid( + values: np.ndarray, + n_mad: float = 5.0, + q: float = 0.99, +) -> np.ndarray: + """ + Robust trimming: + 1. MAD-based filtering (removes extreme structural outliers) + 2. Quantile cap (removes residual tail) + """ + + median = np.median(values) + mad = np.median(np.abs(values - median)) + + if mad > 0: + values = values[np.abs(values - median) <= n_mad * mad] + + if len(values) == 0: + return values + + upper = np.quantile(values, q) + values = values[values <= upper] + + return values + + +def compute_cell_radius( + cell: DataFrame, + x_col: str, + y_col: str, + percentile: float, +) -> float: + """ + Compute the radius of a cell based on the specified percentile of distances + from the cell centroid. + """ + centroid_x: float = float(cell[x_col].mean()) + centroid_y: float = float(cell[y_col].mean()) + + dx: Series = cell[x_col] - centroid_x + dy: Series = cell[y_col] - centroid_y + + distances: NDArray[np.floating] = np.sqrt(dx.to_numpy() ** 2 + dy.to_numpy() ** 2) + + return float(np.percentile(distances, percentile)) + + +def clean_cell_ids(df: DataFrame, col: str) -> DataFrame: + """ + Clean cell IDs in the specified column of the DataFrame. + Removes invalid or missing cell IDs, and converts numeric-like IDs to integers.""" + df = df.copy() + + sample = df[col].dropna().astype(str).head(100) + is_numeric_like = sample.str.fullmatch(r"-?\d+").mean() > 0.8 + + if is_numeric_like: + df[col] = pd.to_numeric(df[col], errors="coerce") + df = df[df[col].notna()].copy() # type: ignore + df = df[df[col] >= 0].copy() # type: ignore + + else: + df[col] = df[col].astype(str) + + df = df[ + (df[col] != "-1") & + (df[col].str.lower() != "nan") & + (df[col].str.lower() != "none") + ].copy() # type: ignore + + return df + + +def load_transcripts(path: Path) -> DataFrame: + """ + Load transcripts from a Parquet or CSV file. + """ + + suffix = path.suffix.lower() + + if suffix == ".parquet": + return pd.read_parquet(path) + + if suffix == ".csv": + return pd.read_csv(path) + + raise ValueError( + f"Unsupported transcript file format '{suffix}'. " + "Supported formats are .parquet and .csv." + ) + + +def estimate_radii_from_cells( + df: DataFrame, + cell_col: str, + x_col: str, + y_col: str, + percentile: float, + min_transcripts: int, +) -> tuple[list[float], int, int]: + """ + Per-cell radius estimation using prior segmentation (column-based). + """ + + df = clean_cell_ids(df, cell_col) + + if df.empty: + raise RuntimeError("No assigned transcripts found after cleaning.") + + radii: list[float] = [] + total_cells = 0 + used_cells = 0 + + grouped = df.groupby(cell_col) + + for _, cell in grouped: + + total_cells += 1 + + if len(cell) < min_transcripts: + continue + + used_cells += 1 + + radius: float = compute_cell_radius(cell, x_col, y_col, percentile) + radii.append(radius) + + return radii, total_cells, used_cells + + +def estimate_radii_from_density( + df: DataFrame, + x_col: str, + y_col: str, + percentile: float, + k_neighbors: int, +) -> tuple[list[float], int, int]: + """ + Density-based radius estimation for the no-prior case. + + Uses the distance to the k-th nearest neighbor of each transcript as a + proxy for local point density, then converts that to a proxy "cell + radius" via the requested percentile. This avoids requiring per-cell + grouping when no prior segmentation (cell assignment) is available. + """ + + coords = df[[x_col, y_col]].to_numpy() + + if len(coords) <= k_neighbors: + raise RuntimeError( + f"Not enough transcripts ({len(coords)}) for k={k_neighbors} " + "nearest-neighbor density estimation." + ) + + tree = cKDTree(coords) + # k_neighbors + 1 because the nearest neighbor of a point is itself (distance 0) + distances, _ = tree.query(coords, k=k_neighbors + 1) + knn_distances = distances[:, -1] + + radius = float(np.percentile(knn_distances, percentile)) + + total_cells = len(coords) + used_cells = len(coords) + + return [radius], total_cells, used_cells + + +def main() -> None: + + args = parse_args() + + df: DataFrame = load_transcripts(args.transcripts) + + required_cols: list[str] = [args.x_column, args.y_column] + if args.cell_column: + required_cols.append(args.cell_column) + + missing: list[str] = [c for c in required_cols if c not in df.columns] + + if missing: + raise ValueError(f"Missing required columns: {missing}") + + if args.cell_column: + radii, total_cells, used_cells = estimate_radii_from_cells( + df, + args.cell_column, + args.x_column, + args.y_column, + args.percentile, + args.min_transcripts, + ) + else: + radii, total_cells, used_cells = estimate_radii_from_density( + df, + args.x_column, + args.y_column, + args.percentile, + args.knn_neighbors, + ) + + if len(radii) == 0: + raise RuntimeError( + "No cells passed filtering. Try lowering --min-transcripts." + ) + + radii_arr: NDArray[np.floating] = np.asarray(radii) + + radii_arr = trim_outliers_hybrid(radii_arr) + + if len(radii_arr) == 0: + raise RuntimeError("All radii removed by outlier filtering.") + + median_radius: float = float(np.median(radii_arr)) + mean_radius: float = float(np.mean(radii_arr)) + std_radius: float = float(np.std(radii_arr)) + min_radius: float = float(np.min(radii_arr)) + max_radius: float = float(np.max(radii_arr)) + + mad: float = median_absolute_deviation(radii_arr) + + percentile_25: float = float(np.percentile(radii_arr, 25)) + percentile_75: float = float(np.percentile(radii_arr, 75)) + percentile_90: float = float(np.percentile(radii_arr, 90)) + percentile_95: float = float(np.percentile(radii_arr, 95)) + + recommended_scale: float = math.ceil( + float( + np.clip( + median_radius, + args.min_scale, + args.max_scale, + ) + ) + ) + + if (args.verbose): + logger.info("========== Baysor Scale Estimation ==========") + + logger.info(f"Transcript file : {args.transcripts}") + logger.info(f"Estimation mode : {'cell-based (prior)' if args.cell_column else 'density-based (no prior)'}") + logger.info(f"Cells detected : {total_cells}") + logger.info(f"Cells used : {used_cells}") + logger.info(f"Minimum transcripts/cell : {args.min_transcripts}") + logger.info(f"Radius percentile : {args.percentile}") + + logger.info("Transcript-cloud radius statistics (µm)") + logger.info("----------------------------------------") + logger.info(f"Median : {median_radius:.2f}") + logger.info(f"Mean : {mean_radius:.2f}") + logger.info(f"MAD : {mad:.2f}") + logger.info(f"Std : {std_radius:.2f}") + logger.info(f"Min : {min_radius:.2f}") + logger.info(f"25% : {percentile_25:.2f}") + logger.info(f"75% : {percentile_75:.2f}") + logger.info(f"90% : {percentile_90:.2f}") + logger.info(f"95% : {percentile_95:.2f}") + logger.info(f"Max : {max_radius:.2f}") + + logger.info("=========================================") + logger.info(f"Recommended Baysor --scale : {recommended_scale:.2f} µm") + logger.info("=========================================") + + print(f"{recommended_scale:.2f}", file=open(f"{args.prefix}_scale_factor.txt", "w")) + + +if __name__ == "__main__": + main() diff --git a/bin/modules/local/utility/estimatescalefactor/main.nf b/bin/modules/local/utility/estimatescalefactor/main.nf new file mode 100644 index 00000000..e2ae3413 --- /dev/null +++ b/bin/modules/local/utility/estimatescalefactor/main.nf @@ -0,0 +1,42 @@ +process BAYSOR_ESTIMATE_SCALE_FACTOR { + tag "$meta.id" + label 'process_single' + + conda "${moduleDir}/environment.yml" + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/35/351697a9ef131734b4aa14d71c7f6263075294013a107685a90a28573f1b3721/data': + 'community.wave.seqera.io/library/pandas_numpy:278960cc5cd28666' }" + + input: + tuple val(meta), path(transcripts) + tuple val(cell_col), val(x_col), val(y_col), val(min_transcripts_per_cell) + + output: + tuple val(meta), stdout, emit: scale_factor + tuple val("${task.process}"), val('python3'), eval("python3 --version | awk '{print $2}'"), topic: versions, emit: versions_python + tuple val("${task.process}"), val('numpy'), eval("python3 -c 'import numpy; print(numpy.__version__)'"), topic: versions, emit: versions_numpy + tuple val("${task.process}"), val('pandas'), eval("python3 -c 'import pandas; print(pandas.__version__)'"), topic: versions, emit: versions_pandas + + when: + task.ext.when == null || task.ext.when + + script: + def args = task.ext.args ?: '' + + """ + baysor_estimate_scale_factor.py \\ + --transcripts ${transcripts} \\ + --cell-column ${cell_col} \\ + --x-column ${x_col} \\ + --y-column ${y_col} \\ + --percentile 90.0 \\ + --min-transcripts ${min_transcripts_per_cell} \\ + --verbose \\ + ${args} + """ + + stub: + """ + echo "7.00" + """ +} diff --git a/bin/stitch_transcripts.py b/bin/stitch_transcripts.py deleted file mode 100755 index 45f057e3..00000000 --- a/bin/stitch_transcripts.py +++ /dev/null @@ -1,848 +0,0 @@ -#!/usr/bin/env python3 -"""Stitch per-patch Baysor segmentation results into unified output. - -Standalone script that replaces the xenium_patch CLI package's stitch -functionality. Uses sopa's solve_conflicts() for overlap resolution. -""" - -from __future__ import annotations - -import argparse -import json -import os -from concurrent.futures import ThreadPoolExecutor -from dataclasses import dataclass -from pathlib import Path - -import geopandas as gpd -import numpy as np -import pyarrow as pa -import pyarrow.compute as pc -import pyarrow.csv as pa_csv -import shapely -from shapely.affinity import translate -from shapely.geometry import mapping, shape -from sopa.segmentation.resolve import solve_conflicts - -# --------------------------------------------------------------------------- -# Inline types (from _types.py) -# --------------------------------------------------------------------------- - - -@dataclass(frozen=True) -class Bounds: - """Axis-aligned bounding box in either pixel or micron coordinates.""" - - x_min: float - x_max: float - y_min: float - y_max: float - - -@dataclass(frozen=True) -class PatchInfo: - """Metadata for a single patch in the grid.""" - - patch_id: str - row: int - col: int - global_bounds_px: Bounds - global_bounds_um: Bounds - core_bounds_px: Bounds - core_bounds_um: Bounds - - -@dataclass -class PatchGridMetadata: - """Full grid metadata, serializable to JSON.""" - - version: str - bundle_path: str - image_height_px: int - image_width_px: int - pixel_size_um: float - transcript_extent_um: Bounds - grid_rows: int - grid_cols: int - overlap_um: float - overlap_px: int - patches: list[PatchInfo] - grid_type: str = "uniform" - - -# --------------------------------------------------------------------------- -# Internal result containers -# --------------------------------------------------------------------------- - - -@dataclass -class _PatchGeoResult: - """Result of parallel GeoJSON processing for a single patch.""" - - features: list[dict] - cell_ids: list[str] - - -@dataclass -class _PatchCsvResult: - """Result of parallel CSV reading for a single patch.""" - - table: pa.Table - has_cell_col: bool - has_x_col: bool - has_y_col: bool - has_gene_col: bool = False - has_feature_name_col: bool = False - - -# --------------------------------------------------------------------------- -# Grid metadata I/O (from grid.py) -# --------------------------------------------------------------------------- - - -def _dict_to_bounds(d: dict) -> Bounds: - return Bounds(d["x_min"], d["x_max"], d["y_min"], d["y_max"]) - - -def load_grid_metadata(input_path: Path) -> PatchGridMetadata: - """Deserialize PatchGridMetadata from JSON. - - Args: - input_path: Path to JSON file to read. - - Returns: - Reconstructed PatchGridMetadata. - """ - with open(input_path) as f: - data = json.load(f) - - patches = [ - PatchInfo( - patch_id=p["patch_id"], - row=p["row"], - col=p["col"], - global_bounds_px=_dict_to_bounds(p["global_bounds_px"]), - global_bounds_um=_dict_to_bounds(p["global_bounds_um"]), - core_bounds_px=_dict_to_bounds(p["core_bounds_px"]), - core_bounds_um=_dict_to_bounds(p["core_bounds_um"]), - ) - for p in data["patches"] - ] - - return PatchGridMetadata( - version=data["version"], - bundle_path=data["bundle_path"], - image_height_px=data["image_height_px"], - image_width_px=data["image_width_px"], - pixel_size_um=data["pixel_size_um"], - transcript_extent_um=_dict_to_bounds(data["transcript_extent_um"]), - grid_rows=data["grid_rows"], - grid_cols=data["grid_cols"], - overlap_um=data["overlap_um"], - overlap_px=data["overlap_px"], - grid_type=data.get("grid_type", "uniform"), - patches=patches, - ) - - -# --------------------------------------------------------------------------- -# GeoJSON I/O (from polygon_io.py) -# --------------------------------------------------------------------------- - - -def _normalize_geometry_collection(geojson: dict) -> dict: - """Convert a GeometryCollection to a FeatureCollection. - - proseg-to-baysor produces a non-standard GeoJSON GeometryCollection where - each geometry object has a custom ``cell`` key (bare integer) instead of - using Feature wrappers. This normalises it to a standard FeatureCollection - with ``id`` and ``properties.cell_id`` on each feature, using the - ``"cell-{N}"`` format that matches the companion CSV. - - Args: - geojson: Parsed GeoJSON dict with type GeometryCollection. - - Returns: - Standard FeatureCollection dict. - """ - features = [] - for geom in geojson.get("geometries", []): - cell_raw = geom.get("cell", "") - cell_id = str(cell_raw) - clean_geom = {k: v for k, v in geom.items() if k != "cell"} - feature = { - "type": "Feature", - "id": cell_id, - "geometry": clean_geom, - "properties": {"cell_id": cell_id}, - } - features.append(feature) - return {"type": "FeatureCollection", "features": features} - - -def read_geojson(geojson_path: Path) -> dict: - """Read a GeoJSON file and normalise to FeatureCollection. - - Handles both standard FeatureCollections and the GeometryCollection - format produced by proseg-to-baysor. - - Args: - geojson_path: Path to the GeoJSON file. - - Returns: - Parsed GeoJSON dict (always a FeatureCollection). - """ - with open(geojson_path) as f: - data = json.load(f) - if data.get("type") == "GeometryCollection": - return _normalize_geometry_collection(data) - return data - - -def transform_polygons(geojson: dict, offset_x: float, offset_y: float) -> dict: - """Shift all polygon coordinates by (offset_x, offset_y). - - Args: - geojson: Input FeatureCollection. - offset_x: Translation in x. - offset_y: Translation in y. - - Returns: - New FeatureCollection with shifted geometries. - """ - features = [] - for feat in geojson.get("features", []): - geom = shape(feat["geometry"]) - shifted = translate(geom, xoff=offset_x, yoff=offset_y) - new_feat = {**feat, "geometry": mapping(shifted)} - features.append(new_feat) - return {"type": "FeatureCollection", "features": features} - - -def write_geojson(geojson: dict, output_path: Path) -> None: - """Write a GeoJSON FeatureCollection. - - Args: - geojson: GeoJSON dict to write. - output_path: Destination path (parent dirs created automatically). - """ - output_path.parent.mkdir(parents=True, exist_ok=True) - with open(output_path, "w") as f: - json.dump(geojson, f) - - -# --------------------------------------------------------------------------- -# Arrow utilities (from _arrow_utils.py) -# --------------------------------------------------------------------------- - - -def float_str_array(f64_array: pa.Array) -> pa.Array: - """Convert a float64 pyarrow array to string using Python's str(float) format. - - pyarrow's built-in cast omits trailing '.0' for whole numbers. This - function ensures output matches str(float(...)) for CSV compatibility. - - Args: - f64_array: Float64 pyarrow array to convert. - - Returns: - String pyarrow array with Python-formatted float values. - """ - return pa.array( - [str(v) if v is not None else None for v in f64_array.to_pylist()], - type=pa.string(), - ) - - -# --------------------------------------------------------------------------- -# Parallel I/O -# --------------------------------------------------------------------------- - - -def _read_and_transform_geojson( - patch: PatchInfo, - patches_dir: Path, - geojson_filename: str, -) -> _PatchGeoResult | None: - """Read, transform GeoJSON for a single patch (no core clipping). - - Args: - patch: Patch metadata. - patches_dir: Root patches directory. - geojson_filename: GeoJSON filename within each patch directory. - - Returns: - _PatchGeoResult with features and cell IDs, or None if no GeoJSON. - """ - geojson_path = patches_dir / patch.patch_id / geojson_filename - if not geojson_path.exists(): - return None - - geojson = read_geojson(geojson_path) - - offset_x = patch.global_bounds_um.x_min - offset_y = patch.global_bounds_um.y_min - geojson = transform_polygons(geojson, offset_x, offset_y) - - features = geojson.get("features", []) - seen: set[str] = set() - cell_ids: list[str] = [] - for feat in features: - old_id = str(feat.get("id", feat.get("properties", {}).get("cell_id", ""))) - if old_id not in seen: - seen.add(old_id) - cell_ids.append(old_id) - - return _PatchGeoResult(features=features, cell_ids=cell_ids) - - -def _read_patch_csv( - patch: PatchInfo, - patches_dir: Path, - csv_filename: str, -) -> _PatchCsvResult | None: - """Read a patch CSV into a pyarrow Table. - - All columns are read as strings to preserve exact formatting. - - Args: - patch: Patch metadata. - patches_dir: Root patches directory. - csv_filename: CSV filename within each patch directory. - - Returns: - _PatchCsvResult with the table and column presence flags, or None. - """ - csv_path = patches_dir / patch.patch_id / csv_filename - if not csv_path.exists(): - return None - - with open(csv_path) as fh: - header_line = fh.readline().strip() - col_names = header_line.split(",") - all_string_types = {name: pa.string() for name in col_names} - - table = pa_csv.read_csv( - csv_path, - convert_options=pa_csv.ConvertOptions( - column_types=all_string_types, - strings_can_be_null=False, - ), - read_options=pa_csv.ReadOptions(use_threads=True), - ) - - return _PatchCsvResult( - table=table, - has_cell_col="cell" in table.column_names, - has_x_col="x" in table.column_names, - has_y_col="y" in table.column_names, - has_gene_col="gene" in table.column_names, - has_feature_name_col="feature_name" in table.column_names, - ) - - -# --------------------------------------------------------------------------- -# CSV processing -# --------------------------------------------------------------------------- - - -def _transform_patch_coords( - csv_result: _PatchCsvResult, - offset_x: float, - offset_y: float, -) -> pa.Table: - """Shift transcript coordinates from local patch space to global space. - - Args: - csv_result: The raw CSV table and column flags. - offset_x: X offset for coordinate transform (microns). - offset_y: Y offset for coordinate transform (microns). - - Returns: - Table with x, y columns shifted to global coordinates. - """ - table = csv_result.table - - if table.num_rows == 0: - return table - - if csv_result.has_x_col: - x_f64 = pc.add( - table.column("x").cast(pa.float64()), - pa.scalar(offset_x, type=pa.float64()), - ) - table = table.set_column( - table.schema.get_field_index("x"), - "x", - float_str_array(x_f64), - ) - if csv_result.has_y_col: - y_f64 = pc.add( - table.column("y").cast(pa.float64()), - pa.scalar(offset_y, type=pa.float64()), - ) - table = table.set_column( - table.schema.get_field_index("y"), - "y", - float_str_array(y_f64), - ) - - return table - - -# --------------------------------------------------------------------------- -# Sopa conflict resolution -# --------------------------------------------------------------------------- - - -def _stitch_sopa_resolve( - metadata: PatchGridMetadata, - geo_results: list[_PatchGeoResult | None], - csv_results: list[_PatchCsvResult | None], - all_geojson_features: list[dict], - all_tables: list[pa.Table], - threshold: float = 0.5, -) -> set[str]: - """Stitch per-patch segmentation using spatial containment assignment. - - 1. Collect ALL non-empty polygons from all patches (no transcript filtering). - 2. Resolve overlapping polygons via sopa's solve_conflicts(). - 3. Assign sequential global cell IDs (cell-1, cell-2, ...). - 4. Spatially assign transcripts to resolved polygons using STRtree. - 5. Noise transcripts (outside all polygons) kept only from their core patch. - - This approach works regardless of whether Baysor's CSV ``cell`` column - matches GeoJSON cell IDs -- all assignment is done by spatial containment. - - Args: - metadata: Grid metadata with patch list. - geo_results: Per-patch GeoJSON results (already in global coords). - csv_results: Per-patch CSV results. - all_geojson_features: Output list to append resolved GeoJSON features. - all_tables: Output list to append processed CSV tables. - threshold: Overlap threshold for sopa's solve_conflicts (0-1). - - Returns: - Set of global cell IDs created by merging overlapping cells. - """ - # --- Phase 1: Collect all polygons from all patches --- - all_polygons: list = [] - patch_indices_list: list[int] = [] - - for i, patch in enumerate(metadata.patches): - geo_result = geo_results[i] - if geo_result is None: - continue - - for feat in geo_result.features: - polygon = shape(feat["geometry"]) - if polygon.is_empty: - continue - if not polygon.is_valid: - polygon = shapely.make_valid(polygon) - if polygon.is_empty: - continue - # make_valid can produce MultiPolygon/GeometryCollection; - # xeniumranger only accepts Polygon, so keep largest component - if polygon.geom_type == "MultiPolygon": - polygon = max(polygon.geoms, key=lambda g: g.area) - elif polygon.geom_type == "GeometryCollection": - polys = [g for g in polygon.geoms if g.geom_type == "Polygon"] - if not polys: - continue - polygon = max(polys, key=lambda g: g.area) - - all_polygons.append(polygon) - patch_indices_list.append(i) - - if not all_polygons: - print("[stitch] No polygons found in any patch") - # Still transform and collect CSVs as noise-only - for i, patch in enumerate(metadata.patches): - csv_result = csv_results[i] - if csv_result is None: - continue - offset_x = patch.global_bounds_um.x_min - offset_y = patch.global_bounds_um.y_min - transformed = _transform_patch_coords(csv_result, offset_x, offset_y) - if transformed.num_rows > 0: - all_tables.append(transformed) - return set() - - # --- Phase 2: Resolve overlapping polygons via sopa --- - patch_idx_array = np.array(patch_indices_list, dtype=np.int64) - input_gdf = gpd.GeoDataFrame(geometry=all_polygons) - resolved_gdf, kept_indices = solve_conflicts( - input_gdf, - threshold=threshold, - patch_indices=patch_idx_array, - return_indices=True, - ) - - # --- Phase 3: Assign global cell IDs to resolved polygons --- - merged_cell_ids: set[str] = set() - kept_arr = np.asarray(kept_indices) - resolved_polys: list = [] - resolved_ids: list[str] = [] - - for rank, orig_idx in enumerate(kept_arr, start=1): - global_id = f"cell-{rank}" - geom = resolved_gdf.geometry.iloc[rank - 1] - - # solve_conflicts union can produce MultiPolygon; keep largest - if geom.geom_type == "MultiPolygon": - geom = max(geom.geoms, key=lambda g: g.area) - elif geom.geom_type == "GeometryCollection": - polys = [g for g in geom.geoms if g.geom_type == "Polygon"] - if not polys: - continue - geom = max(polys, key=lambda g: g.area) - - if orig_idx < 0: - merged_cell_ids.add(global_id) - - resolved_polys.append(geom) - resolved_ids.append(global_id) - - all_geojson_features.append( - { - "type": "Feature", - "id": global_id, - "geometry": mapping(geom), - "properties": {"cell_id": global_id}, - } - ) - - print( - f"[stitch] Resolved {len(all_polygons)} input polygons to " - f"{len(resolved_polys)} cells ({len(merged_cell_ids)} merged)" - ) - - # --- Phase 4: Spatial transcript assignment via STRtree --- - poly_tree = shapely.STRtree(resolved_polys) - - for i, patch in enumerate(metadata.patches): - csv_result = csv_results[i] - if csv_result is None: - continue - - offset_x = patch.global_bounds_um.x_min - offset_y = patch.global_bounds_um.y_min - core = patch.core_bounds_um - - transformed = _transform_patch_coords(csv_result, offset_x, offset_y) - if transformed.num_rows == 0: - continue - - if not csv_result.has_x_col or not csv_result.has_y_col: - all_tables.append(transformed) - continue - - # Get global coordinates for spatial query - gx = transformed.column("x").cast(pa.float64()).to_numpy(zero_copy_only=False) - gy = transformed.column("y").cast(pa.float64()).to_numpy(zero_copy_only=False) - points = shapely.points(gx, gy) - - # Query STRtree: returns (input_indices, tree_indices) - point_hits, poly_hits = poly_tree.query(points, predicate="intersects") - - # Build point -> cell_id mapping (first hit wins) - point_to_cell: dict[int, str] = {} - for pt_idx, poly_idx in zip(point_hits, poly_hits): - if pt_idx not in point_to_cell: - point_to_cell[pt_idx] = resolved_ids[poly_idx] - - # Build cell and is_noise columns - n_rows = transformed.num_rows - cell_arr = [""] * n_rows - is_noise_arr = ["true"] * n_rows - for pt_idx, cell_id in point_to_cell.items(): - cell_arr[pt_idx] = cell_id - is_noise_arr[pt_idx] = "false" - - # Filter noise transcripts to core bounds only - # Assigned transcripts are kept from all patches (dedup later by transcript_id) - in_core = ( - (gx >= core.x_min) - & (gx < core.x_max) - & (gy >= core.y_min) - & (gy < core.y_max) - ) - is_assigned = np.array([c != "" for c in cell_arr]) - keep_mask = pa.array(is_assigned | in_core, type=pa.bool_()) - - filtered = transformed.filter(keep_mask) - cell_arr_filtered = [c for c, k in zip(cell_arr, (is_assigned | in_core)) if k] - is_noise_filtered = [ - n for n, k in zip(is_noise_arr, (is_assigned | in_core)) if k - ] - - if filtered.num_rows == 0: - continue - - # Set cell and is_noise columns - cell_idx = ( - filtered.schema.get_field_index("cell") - if "cell" in filtered.column_names - else None - ) - if cell_idx is not None: - filtered = filtered.set_column( - cell_idx, "cell", pa.array(cell_arr_filtered, type=pa.string()) - ) - else: - filtered = filtered.append_column( - "cell", pa.array(cell_arr_filtered, type=pa.string()) - ) - - noise_idx = ( - filtered.schema.get_field_index("is_noise") - if "is_noise" in filtered.column_names - else None - ) - if noise_idx is not None: - filtered = filtered.set_column( - noise_idx, - "is_noise", - pa.array(is_noise_filtered, type=pa.string()), - ) - else: - filtered = filtered.append_column( - "is_noise", pa.array(is_noise_filtered, type=pa.string()) - ) - - all_tables.append(filtered) - - return merged_cell_ids - - -# --------------------------------------------------------------------------- -# Main orchestrator -# --------------------------------------------------------------------------- - - -def stitch_transcript_assignments( - patches_dir: Path, - output_dir: Path, - csv_filename: str = "segmentation.csv", - geojson_filename: str = "segmentation_polygons.json", - max_workers: int | None = None, - min_transcripts_per_cell: int = 0, -) -> None: - """Stitch per-patch transcript assignments and polygons into unified output. - - For each patch, reads the transcript assignment CSV and polygon GeoJSON. - Cells are deduplicated using sopa's solve_conflicts() which resolves - overlapping cells at patch boundaries based on area overlap ratio. - - Processing is split into a parallel I/O phase (reading GeoJSON and CSV - files via thread pool) and a sequential phase (dedup, global cell ID - assignment, remapping, and concatenation). - - Args: - patches_dir: Directory containing patch subdirectories and patch_grid.json. - output_dir: Output directory for stitched CSV and GeoJSON. - csv_filename: CSV filename within each patch directory. - geojson_filename: GeoJSON filename within each patch directory. - max_workers: Maximum number of threads for parallel I/O. - """ - patches_dir = Path(patches_dir) - output_dir = Path(output_dir) - output_dir.mkdir(parents=True, exist_ok=True) - - metadata = load_grid_metadata(patches_dir / "patch_grid.json") - - n_patches = len(metadata.patches) - if max_workers is None: - max_workers = min(n_patches, os.cpu_count() or 1) - - # ---- Parallel phase: read GeoJSON and CSV files concurrently ---- - with ThreadPoolExecutor(max_workers=max_workers) as executor: - geo_futures = [ - executor.submit( - _read_and_transform_geojson, p, patches_dir, geojson_filename - ) - for p in metadata.patches - ] - csv_futures = [ - executor.submit(_read_patch_csv, p, patches_dir, csv_filename) - for p in metadata.patches - ] - geo_results = [f.result() for f in geo_futures] - csv_results = [f.result() for f in csv_futures] - - # ---- Sequential phase: assign global cell IDs, remap, concatenate ---- - all_tables: list[pa.Table] = [] - all_geojson_features: list[dict] = [] - - _stitch_sopa_resolve( - metadata, - geo_results, - csv_results, - all_geojson_features, - all_tables, - threshold=0.5, - ) - - # Concatenate all patch tables - if all_tables: - merged = pa.concat_tables(all_tables) - - # Deduplicate by transcript_id: prefer assigned over noise - if "transcript_id" in merged.column_names: - if "cell" in merged.column_names: - is_noise = pc.equal(merged.column("cell"), "").cast(pa.int8()) - row_order = pa.array(np.arange(merged.num_rows), type=pa.int64()) - sort_table = pa.table({"_noise": is_noise, "_row": row_order}) - sort_indices = pc.sort_indices( - sort_table, - sort_keys=[("_noise", "ascending"), ("_row", "ascending")], - ) - merged = merged.take(sort_indices) - - tid_np = merged.column("transcript_id").to_numpy(zero_copy_only=False) - _, first_indices = np.unique(tid_np, return_index=True) - first_indices.sort() - merged = merged.take(first_indices) - - # Post-stitch cell filter: drop cells below min_transcripts_per_cell - if min_transcripts_per_cell > 0 and "cell" in merged.column_names: - cell_col = merged.column("cell") - cell_counts: dict[str, int] = {} - for c in cell_col.to_pylist(): - if c: - cell_counts[c] = cell_counts.get(c, 0) + 1 - small_cells = { - cid - for cid, cnt in cell_counts.items() - if cnt < min_transcripts_per_cell - } - if small_cells: - # Reassign transcripts from small cells to noise - new_cell = ["" if c in small_cells else c for c in cell_col.to_pylist()] - new_noise = [ - "true" if c in small_cells else n - for c, n in zip( - cell_col.to_pylist(), - merged.column("is_noise").to_pylist() - if "is_noise" in merged.column_names - else ["false"] * merged.num_rows, - ) - ] - cidx = merged.column_names.index("cell") - merged = merged.set_column( - cidx, "cell", pa.array(new_cell, type=pa.string()) - ) - if "is_noise" in merged.column_names: - nidx = merged.column_names.index("is_noise") - merged = merged.set_column( - nidx, "is_noise", pa.array(new_noise, type=pa.string()) - ) - # Remove filtered cells from GeoJSON - all_geojson_features[:] = [ - f - for f in all_geojson_features - if str(f.get("id", f.get("properties", {}).get("cell_id", ""))) - not in small_cells - ] - print( - f"[stitch] Filtered {len(small_cells)} cells with " - f"<{min_transcripts_per_cell} transcripts" - ) - - # Log assignment stats - if "cell" in merged.column_names: - cell_vals = merged.column("cell").to_pylist() - n_assigned = sum(1 for c in cell_vals if c) - n_noise = sum(1 for c in cell_vals if not c) - print( - f"[stitch] Final: {merged.num_rows} transcripts, " - f"{n_assigned} assigned, {n_noise} noise" - ) - - # Cast is_noise to integer for xeniumranger compatibility - if "is_noise" in merged.column_names: - noise_col = merged.column("is_noise") - if noise_col.type == pa.string(): - lower = pc.utf8_lower(noise_col) - is_true = pc.or_(pc.equal(lower, "true"), pc.equal(lower, "1")) - idx = merged.column_names.index("is_noise") - merged = merged.set_column(idx, "is_noise", is_true.cast(pa.int8())) - - # Write CSV - if merged.num_rows > 0: - csv_out = output_dir / "xr-transcript-metadata.csv" - pa_csv.write_csv( - merged, - csv_out, - write_options=pa_csv.WriteOptions(quoting_style="needed"), - ) - - # Safety net: remove orphan polygons with zero transcripts - if all_geojson_features and all_tables: - csv_cell_ids: set[str] = set() - if "cell" in merged.column_names: - csv_cell_ids = set(c for c in merged.column("cell").to_pylist() if c) - all_geojson_features = [ - f - for f in all_geojson_features - if str(f.get("id", f.get("properties", {}).get("cell_id", ""))) - in csv_cell_ids - ] - - # Write merged GeoJSON - if all_geojson_features: - merged_geo = {"type": "FeatureCollection", "features": all_geojson_features} - write_geojson(merged_geo, output_dir / "xr-cell-polygons.geojson") - - -# --------------------------------------------------------------------------- -# CLI -# --------------------------------------------------------------------------- - - -def main() -> None: - parser = argparse.ArgumentParser( - description="Stitch per-patch Baysor segmentation results into unified output." - ) - parser.add_argument( - "--patches", - type=Path, - required=True, - help="Directory containing patch subdirectories and patch_grid.json", - ) - parser.add_argument( - "--output", - type=Path, - required=True, - help="Output directory for stitched CSV and GeoJSON", - ) - parser.add_argument( - "--csv-filename", - default="segmentation.csv", - help="CSV filename within each patch (default: segmentation.csv)", - ) - parser.add_argument( - "--geojson-filename", - default="segmentation_polygons.json", - help="GeoJSON filename within each patch (default: segmentation_polygons.json)", - ) - parser.add_argument( - "--min-transcripts-per-cell", - type=int, - default=0, - help="Drop cells with fewer transcripts (0 = no filter, default: 0)", - ) - args = parser.parse_args() - - stitch_transcript_assignments( - patches_dir=args.patches, - output_dir=args.output, - csv_filename=args.csv_filename, - geojson_filename=args.geojson_filename, - min_transcripts_per_cell=args.min_transcripts_per_cell, - ) - - -if __name__ == "__main__": - main() diff --git a/bin/utility_parquet_to_csv.py b/bin/utility_parquet2csv.py similarity index 100% rename from bin/utility_parquet_to_csv.py rename to bin/utility_parquet2csv.py diff --git a/bin/utils/__init__.py b/bin/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bin/utils/utils_logger.py b/bin/utils/utils_logger.py new file mode 100644 index 00000000..22ede35d --- /dev/null +++ b/bin/utils/utils_logger.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 + +import logging + +######################################## +# Logging +######################################## + +logging.basicConfig( + level=logging.INFO, + format="[%(asctime)s] [%(levelname)s] %(message)s" +) + +logger = logging.getLogger(__name__) diff --git a/bin/xenium_patch_stitch_transcripts.py b/bin/xenium_patch_stitch_transcripts.py index d9fb8d41..dedc359b 100755 --- a/bin/xenium_patch_stitch_transcripts.py +++ b/bin/xenium_patch_stitch_transcripts.py @@ -640,6 +640,7 @@ def stitch_transcript_assignments( csv_filename: str = "segmentation.csv", geojson_filename: str = "segmentation_polygons.json", max_workers: int | None = None, + min_transcripts_per_cell: int = 0, ) -> None: """Stitch per-patch transcript assignments and polygons into unified output. @@ -657,6 +658,9 @@ def stitch_transcript_assignments( csv_filename: CSV filename within each patch directory. geojson_filename: GeoJSON filename within each patch directory. max_workers: Maximum number of threads for parallel I/O. + min_transcripts_per_cell: Drop cells with fewer transcripts after + stitching; their transcripts are reassigned to noise and their + polygons removed (0 = no filter). """ patches_dir = Path(patches_dir) output_dir = Path(output_dir) @@ -717,6 +721,51 @@ def stitch_transcript_assignments( first_indices.sort() merged = merged.take(first_indices) + # Post-stitch cell filter: drop cells below min_transcripts_per_cell + if min_transcripts_per_cell > 0 and "cell" in merged.column_names: + cell_col = merged.column("cell") + cell_counts: dict[str, int] = {} + for c in cell_col.to_pylist(): + if c: + cell_counts[c] = cell_counts.get(c, 0) + 1 + small_cells = { + cid + for cid, cnt in cell_counts.items() + if cnt < min_transcripts_per_cell + } + if small_cells: + # Reassign transcripts from small cells to noise + new_cell = ["" if c in small_cells else c for c in cell_col.to_pylist()] + new_noise = [ + "true" if c in small_cells else n + for c, n in zip( + cell_col.to_pylist(), + merged.column("is_noise").to_pylist() + if "is_noise" in merged.column_names + else ["false"] * merged.num_rows, + ) + ] + cidx = merged.column_names.index("cell") + merged = merged.set_column( + cidx, "cell", pa.array(new_cell, type=pa.string()) + ) + if "is_noise" in merged.column_names: + nidx = merged.column_names.index("is_noise") + merged = merged.set_column( + nidx, "is_noise", pa.array(new_noise, type=pa.string()) + ) + # Remove filtered cells from GeoJSON + all_geojson_features[:] = [ + f + for f in all_geojson_features + if str(f.get("id", f.get("properties", {}).get("cell_id", ""))) + not in small_cells + ] + print( + f"[stitch] Filtered {len(small_cells)} cells with " + f"<{min_transcripts_per_cell} transcripts" + ) + # Log assignment stats if "cell" in merged.column_names: cell_vals = merged.column("cell").to_pylist() @@ -794,6 +843,12 @@ def main() -> None: default="segmentation_polygons.json", help="GeoJSON filename within each patch (default: segmentation_polygons.json)", ) + parser.add_argument( + "--min-transcripts-per-cell", + type=int, + default=0, + help="Drop cells with fewer transcripts (0 = no filter, default: 0)", + ) args = parser.parse_args() stitch_transcript_assignments( @@ -801,6 +856,7 @@ def main() -> None: output_dir=args.output, csv_filename=args.csv_filename, geojson_filename=args.geojson_filename, + min_transcripts_per_cell=args.min_transcripts_per_cell, ) diff --git a/conf/methods/baysor.config b/conf/methods/baysor.config new file mode 100644 index 00000000..b9cbda71 --- /dev/null +++ b/conf/methods/baysor.config @@ -0,0 +1,25 @@ +params { + filter_transcripts = false + min_qv = 20 + max_x = 24000.0 + min_x = 0.0 + max_y = 24000.0 + min_y = 0.0 + baysor_scale = 30 // Baysor --scale for non-tiled runs + baysor_config = null // path to baysor config TOML (optional) + min_transcripts_per_cell = 10 // post-Baysor cell filtering threshold + baysor_tiling = true // enable tiled Baysor (divide → per-patch Baysor → stitch) + baysor_tiling_micron = 1200 // tile width in microns for Baysor tiling + baysor_tiling_overlap = 200 // overlap between Baysor patches in microns + baysor_tiling_balanced = true // balance transcripts across tiles (merge sparse tiles) + baysor_tiling_scale = 39 // Baysor --scale for tiled runs (larger to compensate for EM on tiles) + baysor_tiling_min_mols_per_cell = 120 // --min-molecules-per-cell for tiled Baysor + baysor_tiling_min_transcripts_per_cell = 50 // post-stitch cell filtering threshold + + // Baysor prior segmentation + // null — no prior (random EM init) + // cells — Xenium bundle cell_id column (column-based, works with tiling) + // cellpose — run Cellpose cell mask as image prior (non-tiled only) + baysor_prior = null + baysor_prior_confidence = 0.2 // prior-segmentation-confidence [0-1] +} diff --git a/conf/methods/ficture.config b/conf/methods/ficture.config new file mode 100644 index 00000000..ab1ae05d --- /dev/null +++ b/conf/methods/ficture.config @@ -0,0 +1,4 @@ +params { + negative_control_regex = null + features = null +} diff --git a/conf/methods/proseg.config b/conf/methods/proseg.config new file mode 100644 index 00000000..e69de29b diff --git a/conf/methods/segger.config b/conf/methods/segger.config new file mode 100644 index 00000000..2cdf6a86 --- /dev/null +++ b/conf/methods/segger.config @@ -0,0 +1,14 @@ +params { + segmentation_refinement = false // wether to run segmentation refinement step (segger) + segger_accelerator = 'cpu' // either 'cuda' or 'cpu' + segger_knn_method = 'kd_tree' // 'cuda' - ensure your system has CUDA installed and configured properly + segger_num_workers = 4 // number of data-loader workers for segger + segger_model = null // path to a pre-trained segger model checkpoint + tile_width = 120 + tile_height = 120 + batch_size_train = 4 // larger batch size can speed up training, but requires more memory + devices = 4 // Use multiple GPUs by increasing the devices parameter to further accelerate training + max_epochs = 200 // increasing #epochs can improve model performance with more learning cycles, but extends training time + batch_size_predict = 1 // larger batch size can speed up training, but requires more memory + cc_analysis = false // to control connected component analysis +} diff --git a/conf/methods/stardist.config b/conf/methods/stardist.config new file mode 100644 index 00000000..919e7b41 --- /dev/null +++ b/conf/methods/stardist.config @@ -0,0 +1,7 @@ +params { + stardist_model = '2D_versatile_fluo' // stardist pretrained model for cell segmentation + stardist_nuclei_model = '2D_versatile_fluo' // stardist pretrained model for nuclei segmentation + stardist_prob_thresh = null // stardist probability threshold + stardist_nms_thresh = null // stardist NMS threshold + stardist_n_tiles = "8 8" // tiling for large images (Xenium images are ~20K×25K) +} diff --git a/conf/methods/xeniumranger.config b/conf/methods/xeniumranger.config new file mode 100644 index 00000000..d91adeff --- /dev/null +++ b/conf/methods/xeniumranger.config @@ -0,0 +1,9 @@ +params { + // Xeniumranger specific + xeniumranger_only = false // to generate redefined bundle with just changing the xr specific params + relabel_genes = false // wether to correct gene names with gene_panel.json + expansion_distance = 5 // default nuclear expansion distance in XOA v2.0 & later + dapi_filter = 100 // adjust the minimum peak intensity to use more nuclei + interior_stain = true // interior stain is enabled by default - false to disable + boundary_stain = true // boundary stain is enabled by default - false to disable +} diff --git a/conf/modules.config b/conf/modules.config index 845f0df4..29169f92 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -79,11 +79,11 @@ process { withName: XENIUMRANGER_IMPORT_SEGMENTATION { publishDir = [ - path: "${params.outdir}/${params.mode}/xeniumranger/import_segementation", + path: "${params.outdir}/${params.mode}/xeniumranger/import_segmentation", mode: params.publish_dir_mode, ] ext.args = {[ - params.expansion_distance != null ? "--expansion-distance=${params.expansion_distance}" : "", + (params.expansion_distance != null && params.mode == "image") ? "--expansion-distance=${params.expansion_distance}" : "", ].join(' ').trim()} } @@ -91,12 +91,9 @@ process { withName: PROSEG { publishDir = [ - path: "${params.outdir}/${params.mode}/proseg/preset", + path: "${params.outdir}/${params.mode}/proseg/", mode: params.publish_dir_mode, ] - ext.args = {[ - params.format != null ? "--${params.format}" : "", - ].join(' ').trim()} } withName: PROSEG2BAYSOR { @@ -111,8 +108,6 @@ process { withName: BAYSOR_RUN { memory = { params.baysor_tiling ? 240.GB * task.attempt : 720.GB } ext.args = "--min-molecules-per-cell ${params.baysor_tiling ? params.baysor_tiling_min_mols_per_cell : 30} --x-column x_location --y-column y_location --z-column z_location --gene-column feature_name" - ext.prior_column = params.baysor_prior == 'cells' ? 'cell_id' : null - ext.prior_confidence = params.baysor_prior != null ? params.baysor_prior_confidence : null publishDir = [ path: { "${params.outdir}/${params.mode}/baysor/run" }, mode: params.publish_dir_mode, @@ -242,9 +237,9 @@ process { ] } - withName: PARQUET_TO_CSV { + withName: PARQUET2CSV { publishDir = [ - path: { "${params.outdir}/${params.mode}/utility/parquet_to_csv" }, + path: { "${params.outdir}/${params.mode}/utility/parquet2csv" }, mode: params.publish_dir_mode, ] } diff --git a/main.nf b/main.nf index ed5c9d42..fb25eb83 100644 --- a/main.nf +++ b/main.nf @@ -15,7 +15,7 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ -include { SPATIALAXE } from './workflows/spatialaxe.nf' +include { SPATIALAXE } from './workflows/spatialaxe.nf' include { PIPELINE_INITIALISATION } from './subworkflows/local/utils_nfcore_spatialaxe_pipeline' include { PIPELINE_COMPLETION } from './subworkflows/local/utils_nfcore_spatialaxe_pipeline' @@ -43,9 +43,11 @@ workflow NFCORE_SPATIALAXE { params.alignment_csv, params.baysor_config, params.baysor_prior, - params.baysor_scale, + // params.baysor_scale, params.baysor_tiling, - params.baysor_tiling_scale, + // params.baysor_tiling_scale, + params.baysor_prior_confidence, + params.min_transcripts_per_cell, params.buffer_samples, params.buffer_size, params.cell_segmentation_only, @@ -78,9 +80,10 @@ workflow NFCORE_SPATIALAXE { params.sharpen_tiff, params.stardist_nuclei_model, params.tiling, - params.xeniumranger_only, + params.xeniumranger_only ) emit: + multiqc_report = SPATIALAXE.out.multiqc_report // channel: /path/to/multiqc_report.html } /* diff --git a/modules.json b/modules.json index c215c2ed..be6e7c9c 100644 --- a/modules.json +++ b/modules.json @@ -5,6 +5,21 @@ "https://github.com/nf-core/modules.git": { "modules": { "nf-core": { + "baysor/preview": { + "branch": "master", + "git_sha": "e20c391087fc926b5b7d7b59bbb7b7744d744049", + "installed_by": ["modules"] + }, + "baysor/run": { + "branch": "master", + "git_sha": "e20c391087fc926b5b7d7b59bbb7b7744d744049", + "installed_by": ["modules"] + }, + "baysor/segfree": { + "branch": "master", + "git_sha": "e20c391087fc926b5b7d7b59bbb7b7744d744049", + "installed_by": ["modules"] + }, "cellpose": { "branch": "master", "git_sha": "0780b963d3ab087e861a4b74e9d0e404115e5352", @@ -19,21 +34,28 @@ }, "opt/flip": { "branch": "master", - "git_sha": "7d3e5c9d3d44", - "installed_by": ["modules"], - "patch": "modules/nf-core/opt/flip/opt-flip.diff" + "git_sha": "f0ce9f694b4d372a663199a810de933651465987", + "installed_by": ["modules", "opt_flip_track_stat"] }, "opt/stat": { "branch": "master", - "git_sha": "7d3e5c9d3d44", - "installed_by": ["modules"], - "patch": "modules/nf-core/opt/stat/opt-stat.diff" + "git_sha": "f0ce9f694b4d372a663199a810de933651465987", + "installed_by": ["modules", "opt_flip_track_stat"] }, "opt/track": { "branch": "master", - "git_sha": "7d3e5c9d3d44", - "installed_by": ["modules"], - "patch": "modules/nf-core/opt/track/opt-track.diff" + "git_sha": "f0ce9f694b4d372a663199a810de933651465987", + "installed_by": ["modules", "opt_flip_track_stat"] + }, + "proseg/proseg": { + "branch": "master", + "git_sha": "d8c5b9b8d5de1b7d468199f09421e4e18c9b176c", + "installed_by": ["modules"] + }, + "proseg/proseg2baysor": { + "branch": "master", + "git_sha": "d8c5b9b8d5de1b7d468199f09421e4e18c9b176c", + "installed_by": ["modules"] }, "stardist": { "branch": "master", @@ -72,6 +94,11 @@ }, "subworkflows": { "nf-core": { + "opt_flip_track_stat": { + "branch": "master", + "git_sha": "f0ce9f694b4d372a663199a810de933651465987", + "installed_by": ["subworkflows"] + }, "utils_nextflow_pipeline": { "branch": "master", "git_sha": "05954dab2ff481bcb999f24455da29a5828af08d", diff --git a/modules/local/baysor/preview/main.nf b/modules/local/baysor/preview/main.nf deleted file mode 100644 index b47bf43c..00000000 --- a/modules/local/baysor/preview/main.nf +++ /dev/null @@ -1,50 +0,0 @@ -process BAYSOR_PREVIEW { - tag "${meta.id}" - label 'process_medium' - - container "khersameesh24/baysor:0.7.1" - - input: - tuple val(meta), path(transcripts), path(config) - - output: - tuple val(meta), path("${prefix}/preview.html"), emit: preview_html - tuple val("${task.process}"), val('baysor'), eval("baysor --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown"), topic: versions, emit: versions_baysor - - when: - task.ext.when == null || task.ext.when - - script: - // Exit if running this module with -profile conda / -profile mamba - if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { - error("BAYSOR_PREVIEW module does not support Conda. Please use Docker / Singularity / Podman instead.") - } - - def args = task.ext.args ?: '' - prefix = task.ext.prefix ?: "${meta.id}" - - """ - export JULIA_NUM_THREADS=${task.cpus} - - mkdir -p ${prefix} - - baysor preview \\ - ${transcripts} \\ - --config ${config} \\ - --output ${prefix}/preview.html - ${args} - """ - - stub: - // Exit if running this module with -profile conda / -profile mamba - if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { - error("BAYSOR_PREVIEW module does not support Conda. Please use Docker / Singularity / Podman instead.") - } - - prefix = task.ext.prefix ?: "${meta.id}" - - """ - mkdir -p ${prefix} - touch ${prefix}/preview.html - """ -} diff --git a/modules/local/baysor/preview/meta.yml b/modules/local/baysor/preview/meta.yml deleted file mode 100644 index f23914e7..00000000 --- a/modules/local/baysor/preview/meta.yml +++ /dev/null @@ -1,72 +0,0 @@ -name: "baysor_preview" -description: Preview run for visualization of data. -keywords: - - exploratory-data-analysis -tools: - - "baysor": - description: "Baysor is a tool that segments cells using spatial gene expression maps. Optionally, segmentation masks can be given as additional input." - homepage: "https://kharchenkolab.github.io/Baysor/dev/" - documentation: "https://kharchenkolab.github.io/Baysor/dev/" - tool_dev_url: "https://github.com/kharchenkolab/Baysor" - doi: "https://doi.org/10.1038/s41587-021-01044-w" - licence: ["MIT license"] - identifier: - -## baysor_preview requires a transcript map of the data and a configuration file with argument values -input: - - - meta: - type: map - description: | - Groovy Map containing sample information - e.g. [ id:'test' ] - - transcripts_csv: - type: file - description: CSV file - pattern: "*.csv" - - - config_toml: - type: file - description: TOML file with config arguments - pattern: "*.toml" - -## segmentation results -output: - preview_html: - - - meta: - type: map - description: | - Groovy Map containing sample information - e.g. [ id:'test' ] - - "preview.html": - type: file - description: segmentation preview - pattern: "preview.html" - versions_baysor: - - - ${task.process}: - type: string - description: The process the versions were collected from - - python: - type: string - description: The tool name - - "baysor --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown": - type: eval - description: The expression to obtain the version of the tool - -topics: - versions: - - - ${task.process}: - type: string - description: The process the versions were collected from - - baysor: - type: string - description: The tool name - - "baysor --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown": - type: eval - description: The expression to obtain the version of the tool - -authors: - - "@sebgoti8" - - "@khersameesh24" -maintainers: - - "@sebgoti8" - - "@khersameesh24" diff --git a/modules/local/baysor/preview/tests/main.nf.test b/modules/local/baysor/preview/tests/main.nf.test deleted file mode 100644 index d3f522d7..00000000 --- a/modules/local/baysor/preview/tests/main.nf.test +++ /dev/null @@ -1,60 +0,0 @@ -nextflow_process { - - name "Test Process BAYSOR PREVIEW" - script "../main.nf" - process "BAYSOR_PREVIEW" - - tag "modules" - tag "modules_local" - tag "baysor" - tag "baysor/preview" - tag "preview" - - test("baysor preview - transcripts.parquet") { - - when { - process { - """ - input[0] = channel.of([ - [id: "test_run_baysor"], - file("https://raw.githubusercontent.com/khersameesh24/test-datasets/baysor/transcripts.parquet", checkIfExists: true), - file("https://raw.githubusercontent.com/khersameesh24/test-datasets/baysor/config/xenium.toml", checkIfExists: true) - ]) - """ - } - } - - then { - assertAll( - { assert process.success }, - { assert file(process.out.preview_html[0][1]).exists() }, - { assert file(process.out.preview_html[0][1]).name == "preview.html" }, - { assert snapshot(process.out.versions_baysor).match("versions") } - ) - } - } - - test("baysor preview stub") { - - options "-stub" - - when { - process { - """ - input[0] = channel.of([ - [id: "test_run_baysor"], - file("https://raw.githubusercontent.com/khersameesh24/test-datasets/baysor/transcripts.parquet", checkIfExists: true), - file("https://raw.githubusercontent.com/khersameesh24/test-datasets/baysor/config/xenium.toml", checkIfExists: true) - ]) - """ - } - } - - then { - assertAll( - { assert process.success }, - { assert snapshot(process.out.versions_baysor).match("versions_stub") } - ) - } - } -} diff --git a/modules/local/baysor/preview/tests/main.nf.test.snap b/modules/local/baysor/preview/tests/main.nf.test.snap deleted file mode 100644 index 08ec9503..00000000 --- a/modules/local/baysor/preview/tests/main.nf.test.snap +++ /dev/null @@ -1,34 +0,0 @@ -{ - "versions_stub": { - "content": [ - [ - [ - "BAYSOR_PREVIEW", - "baysor", - "0.7.1" - ] - ] - ], - "meta": { - "nf-test": "0.9.3", - "nextflow": "25.10.2" - }, - "timestamp": "2026-03-21T03:58:47.202659888" - }, - "versions": { - "content": [ - [ - [ - "BAYSOR_PREVIEW", - "baysor", - "0.7.1" - ] - ] - ], - "meta": { - "nf-test": "0.9.3", - "nextflow": "25.10.2" - }, - "timestamp": "2026-03-21T03:58:35.006511807" - } -} \ No newline at end of file diff --git a/modules/local/baysor/preview/tests/nextflow.config b/modules/local/baysor/preview/tests/nextflow.config deleted file mode 100644 index f8b3a30a..00000000 --- a/modules/local/baysor/preview/tests/nextflow.config +++ /dev/null @@ -1,9 +0,0 @@ -process { - - resourceLimits = [ - cpus: 4, - memory: '8.GB', - time: '2.h', - ] - -} diff --git a/modules/local/baysor/run/main.nf b/modules/local/baysor/run/main.nf deleted file mode 100644 index 53cdfeaa..00000000 --- a/modules/local/baysor/run/main.nf +++ /dev/null @@ -1,67 +0,0 @@ -process BAYSOR_RUN { - tag "${meta.id}" - label 'process_high' - - container "khersameesh24/baysor:0.7.1" - - input: - tuple val(meta), path(transcripts), path(prior_segmentation), path(config), val(scale) - - output: - tuple val(meta), path("${prefix}/segmentation.csv"), path("${prefix}/segmentation_polygons_2d.json"), emit: segmentation - tuple val("${task.process}"), val('baysor'), eval("baysor --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown"), topic: versions, emit: versions_baysor - - when: - task.ext.when == null || task.ext.when - - script: - // Exit if running this module with -profile conda / -profile mamba - if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { - error("BAYSOR_RUN module does not support Conda. Please use Docker / Singularity / Podman instead.") - } - - def args = task.ext.args ?: '' - // Column-based prior (e.g. :cell_id) takes precedence over file-based prior - def prior_col = task.ext.prior_column ? ":${task.ext.prior_column}" : '' - def prior_seg = prior_col ?: (prior_segmentation ? prior_segmentation : '') - def confidence = task.ext.prior_confidence != null ? "--prior-segmentation-confidence=${task.ext.prior_confidence}" : '' - def scaling_factor = scale ? "--scale=${scale}" : '' - def config_arg = config ? "--config=${config}" : '' - prefix = task.ext.prefix ?: "${meta.id}" - - // Build command parts, filtering out empty strings - def cmd_parts = [ - "baysor run", - "${transcripts}", - prior_seg, - scaling_factor, - confidence, - "--output=\"${prefix}/segmentation.csv\"", - config_arg, - "--plot", - "--polygon-format=GeometryCollectionLegacy", - args - ].findAll { cmd -> cmd } - - """ - export JULIA_NUM_THREADS=${task.cpus} - - mkdir -p ${prefix} - - ${cmd_parts.join(' \\\n ')} - """ - - stub: - // Exit if running this module with -profile conda / -profile mamba - if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { - error("BAYSOR_RUN module does not support Conda. Please use Docker / Singularity / Podman instead.") - } - - prefix = task.ext.prefix ?: "${meta.id}" - - """ - mkdir -p ${prefix} - touch "${prefix}/segmentation.csv" - touch "${prefix}/segmentation_polygons_2d.json" - """ -} diff --git a/modules/local/baysor/run/meta.yml b/modules/local/baysor/run/meta.yml deleted file mode 100644 index 1fbdbbaa..00000000 --- a/modules/local/baysor/run/meta.yml +++ /dev/null @@ -1,106 +0,0 @@ -name: "baysor_run" -description: Bayesian segmentation of spatial transcriptomics data. -keywords: - - segmentation - - spatial transcriptomics - - cell clustering - - imaging -tools: - - "baysor": - description: "Baysor is a tool that segments cells using spatial gene expression maps. Optionally, segmentation masks can be given as additional input." - homepage: "https://kharchenkolab.github.io/Baysor/dev/" - documentation: "https://kharchenkolab.github.io/Baysor/dev/" - tool_dev_url: "https://github.com/kharchenkolab/Baysor" - doi: "https://doi.org/10.1038/s41587-021-01044-w" - licence: ["MIT license"] - identifier: - -## Baysor requires a transcript map of the data and a configuration file with argument values -input: - - - meta: - type: map - description: | - Groovy Map containing sample information - e.g. [ id:'test' ] - - transcripts_csv: - type: file - description: CSV file - pattern: "*.csv" - - - config_toml: - type: file - description: TOML file with config arguments - pattern: "*.toml" - -## segmentation results -output: - - segmentation: - - meta: - type: map - description: | - Groovy Map containing sample information - e.g. [ id:'test' ] - - "*segmentation.csv": - type: file - description: results of segmentation - pattern: "segmentation.csv" - - - polygons: - type: file - description: | - File with outlines of segmentation - pattern: "*.json" - - - params: - type: file - description: | - File with full list of parameters used for the model - pattern: "segmentation_params.dump.toml" - - - log: - type: file - description: | - Output file with metadata of running the workflow - pattern: "segmentation_log.log" - - - loom: - type: file - description: | - Loom file with metadata - pattern: "segmentation_counts.loom" - - - stats: - type: file - description: | - Statistics of segmented cells - pattern: "segmentation_cell_stats.csv" - - - versions_baysor: - - - ${task.process}: - type: string - description: The process the versions were collected from - - python: - type: string - description: The tool name - - "baysor --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown": - type: eval - description: The expression to obtain the version of the tool - -topics: - versions: - - - ${task.process}: - type: string - description: The process the versions were collected from - - python: - type: string - description: The tool name - - "baysor --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown": - type: eval - description: The expression to obtain the version of the tool - -authors: - - "@sebgoti8" - - "@khersameesh24" -maintainers: - - "@sebgoti8" - - "@khersameesh24" diff --git a/modules/local/baysor/run/tests/main.nf.test.snap b/modules/local/baysor/run/tests/main.nf.test.snap deleted file mode 100644 index 73d72688..00000000 --- a/modules/local/baysor/run/tests/main.nf.test.snap +++ /dev/null @@ -1,34 +0,0 @@ -{ - "versions_stub": { - "content": [ - [ - [ - "BAYSOR_RUN", - "baysor", - "0.7.1" - ] - ] - ], - "meta": { - "nf-test": "0.9.3", - "nextflow": "25.10.2" - }, - "timestamp": "2026-03-21T03:59:56.882483481" - }, - "versions": { - "content": [ - [ - [ - "BAYSOR_RUN", - "baysor", - "0.7.1" - ] - ] - ], - "meta": { - "nf-test": "0.9.3", - "nextflow": "25.10.2" - }, - "timestamp": "2026-03-21T03:59:44.964721046" - } -} \ No newline at end of file diff --git a/modules/local/baysor/segfree/main.nf b/modules/local/baysor/segfree/main.nf deleted file mode 100644 index eb41fa87..00000000 --- a/modules/local/baysor/segfree/main.nf +++ /dev/null @@ -1,50 +0,0 @@ -process BAYSOR_SEGFREE { - tag "${meta.id}" - label 'process_high' - - container "khersameesh24/baysor:0.7.1" - - input: - tuple val(meta), path(transcripts), path(config) - - output: - tuple val(meta), path("${prefix}/ncvs.loom"), emit: ncvs - tuple val("${task.process}"), val('baysor'), eval("baysor --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown"), topic: versions, emit: versions_baysor - - when: - task.ext.when == null || task.ext.when - - script: - // Exit if running this module with -profile conda / -profile mamba - if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { - error("BAYSOR_SEGFREE module does not support Conda. Please use Docker / Singularity / Podman instead.") - } - - def args = task.ext.args ?: '' - prefix = task.ext.prefix ?: "${meta.id}" - - """ - export JULIA_NUM_THREADS=${task.cpus} - - mkdir -p ${prefix} - - baysor segfree \\ - ${transcripts} \\ - --config ${config} \\ - --output=${prefix}/ncvs.loom \\ - ${args} - """ - - stub: - // Exit if running this module with -profile conda / -profile mamba - if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { - error("BAYSOR_SEGFREE module does not support Conda. Please use Docker / Singularity / Podman instead.") - } - - prefix = task.ext.prefix ?: "${meta.id}" - - """ - mkdir -p ${prefix} - touch "${prefix}/ncvs.loom" - """ -} diff --git a/modules/local/baysor/segfree/meta.yml b/modules/local/baysor/segfree/meta.yml deleted file mode 100644 index e8ccdad2..00000000 --- a/modules/local/baysor/segfree/meta.yml +++ /dev/null @@ -1,75 +0,0 @@ -name: "baysor_segfree" -description: Extract neighborhood composition vectors (NVCs) from a dataset. -keywords: - - neighborhood - - baysor - - segmentation_free -tools: - - "baysor": - description: "Baysor is a tool that segments cells using spatial gene expression maps. Optionally, segmentation masks can be given as additional input." - homepage: "https://kharchenkolab.github.io/Baysor/dev/" - documentation: "https://kharchenkolab.github.io/Baysor/dev/" - tool_dev_url: "https://github.com/kharchenkolab/Baysor" - doi: "https://doi.org/10.1038/s41587-021-01044-w" - licence: ["MIT license"] - identifier: - -## baysor_segfree requires a transcript map of the data and a configuration file with argument values -input: - - - meta: - type: map - description: | - Groovy Map containing sample information - e.g. [ id:'test' ] - - transcripts_csv: - type: file - description: CSV file - pattern: "*.csv" - - config_toml: - type: file - description: TOML file with config arguments - pattern: "*.toml" - -## neighborhood composition vectors -output: - ncvs: - - - meta: - type: map - description: | - Groovy Map containing sample information - e.g. [ id:'test' ] - - "${prefix}/ncvs.loom": - type: file - description: | - Segmenation file in loom format - pattern: "${prefix}/ncvs.loom" - - versions_baysor: - - - ${task.process}: - type: string - description: The process the versions were collected from - - python: - type: string - description: The tool name - - "baysor --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown": - type: eval - description: The expression to obtain the version of the tool - -topics: - versions: - - - ${task.process}: - type: string - description: The process the versions were collected from - - python: - type: string - description: The tool name - - "baysor --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown": - type: eval - description: The expression to obtain the version of the tool - -authors: - - "@sebgoti8" - - "@khersameesh24" -maintainers: - - "@sebgoti8" - - "@khersameesh24" diff --git a/modules/local/baysor/segfree/tests/main.nf.test b/modules/local/baysor/segfree/tests/main.nf.test deleted file mode 100644 index 1fb7d2bc..00000000 --- a/modules/local/baysor/segfree/tests/main.nf.test +++ /dev/null @@ -1,60 +0,0 @@ -nextflow_process { - - name "Test Process BAYSOR SEGFREE" - script "../main.nf" - process "BAYSOR_SEGFREE" - - tag "modules" - tag "modules_local" - tag "baysor" - tag "baysor/segfree" - tag "segmentation-free" - - test("baysor segfree - transcripts.parquet") { - - when { - process { - """ - input[0] = channel.of([ - [id: "test_run_baysor"], - file("https://raw.githubusercontent.com/khersameesh24/test-datasets/baysor/transcripts.parquet", checkIfExists: true), - file("https://raw.githubusercontent.com/khersameesh24/test-datasets/baysor/config/xenium.toml", checkIfExists: true) - ]) - """ - } - } - - then { - assertAll( - { assert process.success }, - { assert file(process.out.ncvs[0][1]).exists() }, - { assert file(process.out.ncvs[0][1]).name == "ncvs.loom" }, - { assert snapshot(process.out.versions_baysor).match("versions") } - ) - } - } - - test("baysor run stub") { - - options "-stub" - - when { - process { - """ - input[0] = channel.of([ - [id: "test_run_baysor"], - file("https://raw.githubusercontent.com/khersameesh24/test-datasets/baysor/transcripts.parquet", checkIfExists: true), - file("https://raw.githubusercontent.com/khersameesh24/test-datasets/baysor/config/xenium.toml", checkIfExists: true) - ]) - """ - } - } - - then { - assertAll( - { assert process.success }, - { assert snapshot(process.out.versions_baysor).match("versions_stub") } - ) - } - } -} diff --git a/modules/local/baysor/segfree/tests/main.nf.test.snap b/modules/local/baysor/segfree/tests/main.nf.test.snap deleted file mode 100644 index 075e13ea..00000000 --- a/modules/local/baysor/segfree/tests/main.nf.test.snap +++ /dev/null @@ -1,34 +0,0 @@ -{ - "versions_stub": { - "content": [ - [ - [ - "BAYSOR_SEGFREE", - "baysor", - "0.7.1" - ] - ] - ], - "meta": { - "nf-test": "0.9.3", - "nextflow": "25.10.2" - }, - "timestamp": "2026-03-21T04:00:54.745153684" - }, - "versions": { - "content": [ - [ - [ - "BAYSOR_SEGFREE", - "baysor", - "0.7.1" - ] - ] - ], - "meta": { - "nf-test": "0.9.3", - "nextflow": "25.10.2" - }, - "timestamp": "2026-03-21T04:00:42.542466144" - } -} \ No newline at end of file diff --git a/modules/local/parquet_to_csv/main.nf b/modules/local/parquet_to_csv/main.nf deleted file mode 100644 index 26c613e3..00000000 --- a/modules/local/parquet_to_csv/main.nf +++ /dev/null @@ -1,33 +0,0 @@ -process PARQUET_TO_CSV { - tag "$meta.id" - label 'process_low' - - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? - 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/f9/f9c8f3a2de4e2aa94500011f7d7d09276e9b6f2d79ee8737c9098fe22d4649bc/data' : - 'community.wave.seqera.io/library/sopa_procps-ng_pyarrow:c9ce8cd2ede79d72' }" - - input: - tuple val(meta), path(parquet) - - output: - tuple val(meta), path("transcripts.csv"), emit: csv - tuple val("${task.process}"), val('pyarrow'), eval("python3 -c 'import pyarrow; print(pyarrow.__version__)'"), topic: versions, emit: versions_pyarrow - - when: - task.ext.when == null || task.ext.when - - script: - """ - python3 -c " -import pyarrow.parquet as pq -import pyarrow.csv as pa_csv -t = pq.read_table('${parquet}') -pa_csv.write_csv(t, 'transcripts.csv') -" - """ - - stub: - """ - touch transcripts.csv - """ -} diff --git a/modules/local/parquet_to_csv/meta.yml b/modules/local/parquet_to_csv/meta.yml deleted file mode 100644 index c3ab2506..00000000 --- a/modules/local/parquet_to_csv/meta.yml +++ /dev/null @@ -1,70 +0,0 @@ -name: "parquet_to_csv" -description: Convert parquet file to CSV for tools with old Parquet readers. -keywords: - - xenium - - parquet - - csv - - transcripts -tools: - - "pyarrow": - description: | - Python library providing a Pythonic API for Apache Arrow, - including fast Parquet and CSV I/O. - homepage: "https://arrow.apache.org/docs/python/" - documentation: "https://arrow.apache.org/docs/python/" - tool_dev_url: "https://github.com/apache/arrow" - doi: "no DOI available" - licence: ["Apache-2.0"] - identifier: "" - -input: - - - meta: - type: map - description: | - Groovy Map containing sample information - e.g. [ id:'test' ] - - parquet: - type: file - description: Parquet file to convert. - pattern: "*.parquet" - ontologies: [] - -output: - csv: - - - meta: - type: map - description: | - Groovy Map containing sample information - e.g. [ id:'test' ] - - "transcripts.csv": - type: file - description: Converted CSV file. - pattern: "transcripts.csv" - ontologies: [] - versions_pyarrow: - - - ${task.process}: - type: string - description: The process the versions were collected from - - pyarrow: - type: string - description: The tool name - - "python3 -c 'import pyarrow; print(pyarrow.__version__)'": - type: eval - description: The expression to obtain the version of the tool - -topics: - versions: - - - ${task.process}: - type: string - description: The process the versions were collected from - - pyarrow: - type: string - description: The tool name - - "python3 -c 'import pyarrow; print(pyarrow.__version__)'": - type: eval - description: The expression to obtain the version of the tool - -authors: - - "@an-altosian" -maintainers: - - "@an-altosian" diff --git a/modules/local/proseg/preset/main.nf b/modules/local/proseg/preset/main.nf deleted file mode 100644 index 553801ba..00000000 --- a/modules/local/proseg/preset/main.nf +++ /dev/null @@ -1,65 +0,0 @@ -process PROSEG { - tag "${meta.id}" - label 'process_high' - - container "ghcr.io/dcjones/proseg:v3.1.0" - - input: - tuple val(meta), path(transcripts) - - output: - tuple val(meta), path("${prefix}/cell-polygons.geojson.gz"), path("${prefix}/transcript-metadata.csv.gz"), emit: seg_outs - tuple val(meta), path("${prefix}/proseg-output.zarr"), emit: zarr - tuple val("${task.process}"), val('proseg'), eval("proseg --version | sed 's/proseg //'"), topic: versions, emit: versions_proseg - - when: - task.ext.when == null || task.ext.when - - script: - // Exit if running this module with -profile conda / -profile mamba - if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { - error("PROSEG module does not support Conda. Please use Docker / Singularity / Podman instead.") - } - - def args = task.ext.args ?: '' - prefix = task.ext.prefix ?: "${meta.id}" - - """ - mkdir -p ${prefix} - - proseg \\ - ${args} \\ - ${transcripts} \\ - --nthreads ${task.cpus} \\ - --output-expected-counts ${prefix}/expected-counts.csv.gz \\ - --output-cell-metadata ${prefix}/cell-metadata.csv.gz \\ - --output-transcript-metadata ${prefix}/transcript-metadata.csv.gz \\ - --output-gene-metadata ${prefix}/gene-metadata.csv.gz \\ - --output-rates ${prefix}/rates.csv.gz \\ - --output-cell-polygons ${prefix}/cell-polygons.geojson.gz \\ - --output-cell-polygon-layers ${prefix}/cell-polygons-layers.geojson.gz \\ - --output-union-cell-polygons ${prefix}/union-cell-polygons.geojson.gz \\ - --output-spatialdata ${prefix}/proseg-output.zarr - """ - - stub: - // Exit if running this module with -profile conda / -profile mamba - if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { - error("PROSEG module does not support Conda. Please use Docker / Singularity / Podman instead.") - } - - prefix = task.ext.prefix ?: "${meta.id}" - - """ - mkdir -p ${prefix}/ - touch "${prefix}/expected-counts.csv.gz" - touch "${prefix}/cell-metadata.csv.gz" - touch "${prefix}/transcript-metadata.csv.gz" - touch "${prefix}/gene-metadata.csv.gz" - touch "${prefix}/rates.csv.gz" - touch "${prefix}/cell-polygons.geojson.gz" - touch "${prefix}/cell-polygons-layers.geojson.gz" - touch "${prefix}/union-cell-polygons.geojson.gz" - mkdir -p "${prefix}/proseg-output.zarr" - """ -} diff --git a/modules/local/proseg/preset/meta.yml b/modules/local/proseg/preset/meta.yml deleted file mode 100644 index 524b5ca0..00000000 --- a/modules/local/proseg/preset/meta.yml +++ /dev/null @@ -1,76 +0,0 @@ -name: "proseg" -description: Probabilistic cell segmentation for in situ spatial transcriptomics -keywords: - - segmentation - - cell segmentation - - spatialomics - - probabilistic segmentation - - in situ spatial transcriptomics -tools: - - "proseg": - description: "Proseg (probabilistic segmentation) is a cell segmentation method for in situ spatial transcriptomics. Xenium, CosMx, and MERSCOPE platforms are currently supported." - homepage: "https://github.com/dcjones/proseg/tree/main" - documentation: "https://github.com/dcjones/proseg/blob/main/README.md" - tool_dev_url: "https://github.com/dcjones/proseg" - doi: "" - licence: ["GNU Public License"] - -input: - - - meta: - type: map - description: | - Groovy Map containing run information - e.g. `[ id:'run_id']` - - transcripts: - type: file - description: | - File containing the transcript position - pattern: "transcripts.csv.gz" - -output: - - - meta: - type: map - description: | - Groovy Map containing run information - e.g. `[ id:'run_id']` - - cell_polygons: - type: file - description: 2D polygons for each cell in GeoJSON format. These are flattened from 3D - pattern: "cell-polygons.geojson.gz" - - - expected_counts: - type: file - description: cell-by-gene count matrix - pattern: "expected-counts.csv.gz" - - - cell_metadata: - type: file - description: Cell centroids, volume, and other information - pattern: "cell-metadata.csv.gz" - - - transcript_metadata: - type: file - description: Transcript ids, genes, revised positions, assignment probability - pattern: "transcript-metadata.csv.gz" - - - gene_metadata: - type: file - description: Per-gene summary statistics - pattern: "gene-metadata.csv.gz" - - - rates: - type: file - description: Cell-by-gene Poisson rate parameters - pattern: "rates.csv.gz" - - - cell_polygon_layers: - type: file - description: A separate, non-overlapping cell polygon for each z-layer, preserving 3D segmentation - pattern: "cell-polygons-layers.geojson.gz" - - - cell_hulls: - type: file - description: Convex hulls around assigned transcripts - pattern: "cell-hulls.geojson.gz" - - - versions: - type: file - description: File containing software versions - pattern: "versions.yml" - -authors: - - "@khersameesh24" -maintainers: - - "@khersameesh24" diff --git a/modules/local/proseg/preset/tests/main.nf.test.snap b/modules/local/proseg/preset/tests/main.nf.test.snap deleted file mode 100644 index 944325aa..00000000 --- a/modules/local/proseg/preset/tests/main.nf.test.snap +++ /dev/null @@ -1,34 +0,0 @@ -{ - "versions_stub": { - "content": [ - [ - [ - "PROSEG", - "proseg", - "3.1.0" - ] - ] - ], - "meta": { - "nf-test": "0.9.3", - "nextflow": "25.10.2" - }, - "timestamp": "2026-03-21T04:01:19.610456233" - }, - "versions": { - "content": [ - [ - [ - "PROSEG", - "proseg", - "3.1.0" - ] - ] - ], - "meta": { - "nf-test": "0.9.3", - "nextflow": "25.10.2" - }, - "timestamp": "2026-03-21T04:01:12.114004958" - } -} \ No newline at end of file diff --git a/modules/local/proseg/preset/tests/nextflow.config b/modules/local/proseg/preset/tests/nextflow.config deleted file mode 100644 index f8b3a30a..00000000 --- a/modules/local/proseg/preset/tests/nextflow.config +++ /dev/null @@ -1,9 +0,0 @@ -process { - - resourceLimits = [ - cpus: 4, - memory: '8.GB', - time: '2.h', - ] - -} diff --git a/modules/local/proseg/proseg2baysor/main.nf b/modules/local/proseg/proseg2baysor/main.nf deleted file mode 100644 index 1a0c8b38..00000000 --- a/modules/local/proseg/proseg2baysor/main.nf +++ /dev/null @@ -1,47 +0,0 @@ -process PROSEG2BAYSOR { - tag "$meta.id" - label 'process_high' - - container "ghcr.io/dcjones/proseg:v3.1.0" - - input: - tuple val(meta), path(zarr_dir) - - output: - tuple val(meta), path("${prefix}/cell-polygons.geojson") , emit: xr_polygons - tuple val(meta), path("${prefix}/transcript-metadata.csv"), emit: xr_metadata - tuple val("${task.process}"), val('proseg'), eval("proseg --version | sed 's/proseg //'"), topic: versions, emit: versions_proseg - - script: - // Exit if running this module with -profile conda / -profile mamba - if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { - error "PROSEG2BAYSOR module does not support Conda. Please use Docker / Singularity / Podman instead." - } - - def args = task.ext.args ?: '' - prefix = task.ext.prefix ?: "${meta.id}" - - """ - mkdir -p ${prefix} - - proseg-to-baysor \\ - ${zarr_dir} \\ - --output-transcript-metadata ${prefix}/transcript-metadata.csv \\ - --output-cell-polygons ${prefix}/cell-polygons.geojson \\ - ${args} - """ - - stub: - // Exit if running this module with -profile conda / -profile mamba - if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { - error "PROSEG2BAYSOR module does not support Conda. Please use Docker / Singularity / Podman instead." - } - - prefix = task.ext.prefix ?: "${meta.id}" - - """ - mkdir -p ${prefix} - touch "${prefix}/transcript-metadata.csv" - touch "${prefix}/cell-polygons.geojson" - """ -} diff --git a/modules/local/proseg/proseg2baysor/meta.yml b/modules/local/proseg/proseg2baysor/meta.yml deleted file mode 100644 index 3d5cfac4..00000000 --- a/modules/local/proseg/proseg2baysor/meta.yml +++ /dev/null @@ -1,80 +0,0 @@ -name: "proseg2baysor" -description: Probabilistic cell segmentation for in situ spatial transcriptomics -keywords: - - segmentation - - cell segmentation - - spatialomics - - probabilistic segmentation - - in situ spatial transcriptomics -tools: - - "proseg": - description: "Proseg (probabilistic segmentation) is a cell segmentation method for in situ spatial transcriptomics. Xenium, CosMx, and MERSCOPE platforms are currently supported." - homepage: "https://github.com/dcjones/proseg/tree/main" - documentation: "https://github.com/dcjones/proseg/blob/main/README.md" - tool_dev_url: "https://github.com/dcjones/proseg" - doi: "" - licence: ["GNU Public License"] - -input: - - - meta: - type: map - description: | - Groovy Map containing run information - e.g. `[ id:'run_id']` - - cell_polygons: - type: file - description: | - Cell polygons output file from the proseg xenium (format) run - pattern: "cell-polygons.geojson.gz" - - - transcript_metadata: - type: file - description: | - Transcript metadata file output file from the proseg xenium (format) run - pattern: "transcript-metadata.csv.gz" -output: - xr_polygons: - - - meta: - type: map - description: | - Groovy Map containing run information - e.g. `[ id:'run_id']` - - "${prefix}/cell-polygons.geojson": - type: file - description: 2D polygons for each cell in GeoJSON format. These are flattened from 3D - pattern: "xr-cell-polygons.geojson" - xr_metadata: - - - meta: - type: map - description: | - Groovy Map containing run information - e.g. `[ id:'run_id']` - - "${prefix}/transcript-metadata.csv": - type: file - description: Transcript ids, genes, revised positions, assignment probability - pattern: "xr-transcript-metadata.csv" - versions_proseg: - - - ${task.process}: - type: string - description: The process the versions were collected from - - python: - type: string - description: The tool name - - "proseg --version | sed 's/proseg //'": - type: eval - description: The expression to obtain the version of the tool -topics: - versions: - - - ${task.process}: - type: string - description: The process the versions were collected from - - python: - type: string - description: The tool name - - "proseg --version | sed 's/proseg //'": - type: eval - description: The expression to obtain the version of the tool - -authors: - - "@khersameesh24" -maintainers: - - "@khersameesh24" diff --git a/modules/local/proseg/proseg2baysor/tests/main.nf.test b/modules/local/proseg/proseg2baysor/tests/main.nf.test deleted file mode 100644 index 039217e0..00000000 --- a/modules/local/proseg/proseg2baysor/tests/main.nf.test +++ /dev/null @@ -1,72 +0,0 @@ -nextflow_process { - - name "Test Process PROSEG" - script "../main.nf" - process "PROSEG2BAYSOR" - - tag "modules" - tag "modules_nfcore" - tag "proseg" - tag "segmentation" - tag "cell_segmentation" - - - setup { - run("PROSEG") { - script "modules/local/proseg/preset/main.nf" - process { - """ - input[0] = [ - [id: "test_run_proseg"], - file(params.modules_testdata_base_path + "spatial_omics/xenium/homo_sapiens/spatial_gene_expression.csv", checkIfExists: true) - ] - """ - } - } - } - - test("proseg2baysor - cell_polygons, transcript_metadata") { - - when { - process { - """ - input[0] = channel.of([ - [id: "test_run_proseg2baysor"], - ]).combine(PROSEG.out.seg_outs, by: 0) - """ - } - } - - then { - assertAll( - { assert process.success }, - { assert snapshot(process.out).match() } - ) - } - - } - - test("proseg2baysor stub") { - - options "-stub" - - when { - process { - """ - input[0] = channel.of([ - [id: "test_run_proseg2baysor"], - ]).combine(PROSEG.out.seg_outs, by: 0) - """ - } - } - - then { - assertAll( - { assert process.success }, - { assert snapshot(process.out).match() } - ) - } - - } - -} diff --git a/modules/local/proseg/proseg2baysor/tests/main.nf.test.snap b/modules/local/proseg/proseg2baysor/tests/main.nf.test.snap deleted file mode 100644 index 7dff8302..00000000 --- a/modules/local/proseg/proseg2baysor/tests/main.nf.test.snap +++ /dev/null @@ -1,60 +0,0 @@ -{ - "proseg2baysor - cell_polygons, transcript_metadata": { - "content": [ - { - "0": [ - - ], - "1": [ - - ], - "2": [ - - ], - "versions_proseg": [ - - ], - "xr_metadata": [ - - ], - "xr_polygons": [ - - ] - } - ], - "meta": { - "nf-test": "0.9.3", - "nextflow": "25.10.2" - }, - "timestamp": "2026-03-21T03:50:54.118409704" - }, - "proseg2baysor stub": { - "content": [ - { - "0": [ - - ], - "1": [ - - ], - "2": [ - - ], - "versions_proseg": [ - - ], - "xr_metadata": [ - - ], - "xr_polygons": [ - - ] - } - ], - "meta": { - "nf-test": "0.9.3", - "nextflow": "25.10.2" - }, - "timestamp": "2026-03-21T03:51:01.546798675" - } -} \ No newline at end of file diff --git a/modules/local/proseg/proseg2baysor/tests/nextflow.config b/modules/local/proseg/proseg2baysor/tests/nextflow.config deleted file mode 100644 index f8b3a30a..00000000 --- a/modules/local/proseg/proseg2baysor/tests/nextflow.config +++ /dev/null @@ -1,9 +0,0 @@ -process { - - resourceLimits = [ - cpus: 4, - memory: '8.GB', - time: '2.h', - ] - -} diff --git a/modules/local/resolift/tests/main.nf.test.snap b/modules/local/resolift/tests/main.nf.test.snap index 8972d043..221af456 100644 --- a/modules/local/resolift/tests/main.nf.test.snap +++ b/modules/local/resolift/tests/main.nf.test.snap @@ -34,11 +34,11 @@ ] } ], + "timestamp": "2026-03-22T16:00:00.000000000", "meta": { "nf-test": "0.9.3", "nextflow": "25.10.2" - }, - "timestamp": "2026-03-22T16:00:00.000000000" + } }, "resolift tif": { "content": [ @@ -75,10 +75,10 @@ ] } ], + "timestamp": "2026-03-22T16:00:00.000000000", "meta": { "nf-test": "0.9.3", "nextflow": "25.10.2" - }, - "timestamp": "2026-03-22T16:00:00.000000000" + } } } \ No newline at end of file diff --git a/modules/local/spatialdata/write/main.nf b/modules/local/spatialdata/write/main.nf index 6caed6c1..a2ce424d 100644 --- a/modules/local/spatialdata/write/main.nf +++ b/modules/local/spatialdata/write/main.nf @@ -35,6 +35,7 @@ process SPATIALDATA_WRITE { --output-folder ${outputfolder} \\ --segmented-object ${segmented_object} \\ --coordinate-space ${coordinate_space} \\ + --format xenium \\ ${args} """ diff --git a/modules/local/baysor/create_dataset/main.nf b/modules/local/utility/create_dataset/main.nf similarity index 100% rename from modules/local/baysor/create_dataset/main.nf rename to modules/local/utility/create_dataset/main.nf diff --git a/modules/local/baysor/create_dataset/meta.yml b/modules/local/utility/create_dataset/meta.yml similarity index 100% rename from modules/local/baysor/create_dataset/meta.yml rename to modules/local/utility/create_dataset/meta.yml diff --git a/modules/local/baysor/create_dataset/tests/main.nf.test b/modules/local/utility/create_dataset/tests/main.nf.test similarity index 100% rename from modules/local/baysor/create_dataset/tests/main.nf.test rename to modules/local/utility/create_dataset/tests/main.nf.test diff --git a/modules/local/baysor/create_dataset/tests/main.nf.test.snap b/modules/local/utility/create_dataset/tests/main.nf.test.snap similarity index 100% rename from modules/local/baysor/create_dataset/tests/main.nf.test.snap rename to modules/local/utility/create_dataset/tests/main.nf.test.snap diff --git a/modules/local/baysor/create_dataset/tests/nextflow.config b/modules/local/utility/create_dataset/tests/nextflow.config similarity index 100% rename from modules/local/baysor/create_dataset/tests/nextflow.config rename to modules/local/utility/create_dataset/tests/nextflow.config diff --git a/modules/local/utility/estimatescalefactor/environment.yml b/modules/local/utility/estimatescalefactor/environment.yml new file mode 100644 index 00000000..2a8d01a2 --- /dev/null +++ b/modules/local/utility/estimatescalefactor/environment.yml @@ -0,0 +1,9 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - conda-forge::numpy=2.5.1 + - conda-forge::pandas=3.0.3 + - conda-forge::scipy=1.18.0 diff --git a/modules/local/utility/estimatescalefactor/main.nf b/modules/local/utility/estimatescalefactor/main.nf new file mode 100644 index 00000000..4f3cba12 --- /dev/null +++ b/modules/local/utility/estimatescalefactor/main.nf @@ -0,0 +1,50 @@ +process BAYSOR_ESTIMATE_SCALE_FACTOR { + tag "$meta.id" + label 'process_single' + + conda "${moduleDir}/environment.yml" + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/52/52405e3fa5becaa3347716386a108ca08fc67a416aa0a86115d8e6102ec1700d/data': + 'community.wave.seqera.io/library/numpy_pandas_scipy:0e1d34285644fc85' }" + + input: + tuple val(meta), path(transcripts) + val(cell_col) + val(x_col) + val(y_col) + val(min_transcripts_per_cell) + + output: + tuple val(meta), path("${prefix}_scale_factor.txt"), emit: scale_factor + tuple val("${task.process}"), val('numpy'), eval("python3 -c 'import numpy; print(numpy.__version__)'"), topic: versions, emit: versions_numpy + tuple val("${task.process}"), val('pandas'), eval("python3 -c 'import pandas; print(pandas.__version__)'"), topic: versions, emit: versions_pandas + tuple val("${task.process}"), val('python3'), eval("python3 -c 'import platform; print(platform.python_version())'"), topic: versions, emit: versions_python + + when: + task.ext.when == null || task.ext.when + + script: + def args = task.ext.args ?: '' + def cell_column = cell_col ? "--cell-column ${cell_col}" : '' + prefix = task.ext.prefix ?: "${meta.id}" + + """ + baysor_estimate_scale_factor.py \\ + --transcripts ${transcripts} \\ + ${cell_column} \\ + --x-column ${x_col} \\ + --y-column ${y_col} \\ + --percentile 90.0 \\ + --min-transcripts ${min_transcripts_per_cell} \\ + --prefix ${prefix} \\ + --verbose \\ + ${args} + """ + + stub: + prefix = task.ext.prefix ?: "${meta.id}" + + """ + touch ${prefix}_scale_factor.txt + """ +} diff --git a/modules/local/utility/estimatescalefactor/tests/main.nf.test b/modules/local/utility/estimatescalefactor/tests/main.nf.test new file mode 100644 index 00000000..f9ddae53 --- /dev/null +++ b/modules/local/utility/estimatescalefactor/tests/main.nf.test @@ -0,0 +1,71 @@ +nextflow_process { + + name "Test Process BAYSOR_ESTIMATE_SCALE_FACTOR" + script "../main.nf" + process "BAYSOR_ESTIMATE_SCALE_FACTOR" + + tag "modules" + tag "modules_local" + tag "utility" + tag "utility/estimatescalefactor" + + + test("xenium transcripts - csv") { + + when { + process { + """ + input[0] = [ + [id: "test_estimatescalefactor"], + file(params.modules_testdata_base_path + "spatial_omics/xenium/homo_sapiens/spatial_gene_expression.csv", checkIfExists: true) + ] + input[1] = "cell_id" + input[2] = "x_location" + input[3] = "y_location" + input[4] = "10" + """ + } + } + + then { + assert process.success + assertAll( + { assert snapshot( + process.out, + process.out.findAll { key, val -> key.startsWith('versions') } + ).match() } + ) + } + + } + + + test("xenium transcripts - csv - stub") { + + options "-stub" + + when { + process { + """ + input[0] = [ + [id: "test_estimatescalefactor"], + file(params.modules_testdata_base_path + "spatial_omics/xenium/homo_sapiens/spatial_gene_expression.csv", checkIfExists: true) + ] + input[1] = "cell_id" + input[2] = "x_location" + input[3] = "y_location" + input[4] = "10" + """ + } + } + + then { + assert process.success + assertAll( + { assert snapshot(process.out).match() } + ) + } + + } + +} diff --git a/modules/local/utility/estimatescalefactor/tests/main.nf.test.snap b/modules/local/utility/estimatescalefactor/tests/main.nf.test.snap new file mode 100644 index 00000000..8c22859e --- /dev/null +++ b/modules/local/utility/estimatescalefactor/tests/main.nf.test.snap @@ -0,0 +1,163 @@ +{ + "xenium transcripts - csv - stub": { + "content": [ + { + "0": [ + [ + { + "id": "test_estimatescalefactor" + }, + "test_estimatescalefactor_scale_factor.txt:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "1": [ + [ + "BAYSOR_ESTIMATE_SCALE_FACTOR", + "numpy", + "2.5.1" + ] + ], + "2": [ + [ + "BAYSOR_ESTIMATE_SCALE_FACTOR", + "pandas", + "3.0.3" + ] + ], + "3": [ + [ + "BAYSOR_ESTIMATE_SCALE_FACTOR", + "python3", + "3.14.6" + ] + ], + "scale_factor": [ + [ + { + "id": "test_estimatescalefactor" + }, + "test_estimatescalefactor_scale_factor.txt:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "versions_numpy": [ + [ + "BAYSOR_ESTIMATE_SCALE_FACTOR", + "numpy", + "2.5.1" + ] + ], + "versions_pandas": [ + [ + "BAYSOR_ESTIMATE_SCALE_FACTOR", + "pandas", + "3.0.3" + ] + ], + "versions_python": [ + [ + "BAYSOR_ESTIMATE_SCALE_FACTOR", + "python3", + "3.14.6" + ] + ] + } + ], + "timestamp": "2026-07-12T21:45:33.034603681", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.3" + } + }, + "xenium transcripts - csv": { + "content": [ + { + "0": [ + [ + { + "id": "test_estimatescalefactor" + }, + "test_estimatescalefactor_scale_factor.txt:md5,90df35911f2ec38d497a010ef60a5247" + ] + ], + "1": [ + [ + "BAYSOR_ESTIMATE_SCALE_FACTOR", + "numpy", + "2.5.1" + ] + ], + "2": [ + [ + "BAYSOR_ESTIMATE_SCALE_FACTOR", + "pandas", + "3.0.3" + ] + ], + "3": [ + [ + "BAYSOR_ESTIMATE_SCALE_FACTOR", + "python3", + "3.14.6" + ] + ], + "scale_factor": [ + [ + { + "id": "test_estimatescalefactor" + }, + "test_estimatescalefactor_scale_factor.txt:md5,90df35911f2ec38d497a010ef60a5247" + ] + ], + "versions_numpy": [ + [ + "BAYSOR_ESTIMATE_SCALE_FACTOR", + "numpy", + "2.5.1" + ] + ], + "versions_pandas": [ + [ + "BAYSOR_ESTIMATE_SCALE_FACTOR", + "pandas", + "3.0.3" + ] + ], + "versions_python": [ + [ + "BAYSOR_ESTIMATE_SCALE_FACTOR", + "python3", + "3.14.6" + ] + ] + }, + { + "versions_numpy": [ + [ + "BAYSOR_ESTIMATE_SCALE_FACTOR", + "numpy", + "2.5.1" + ] + ], + "versions_pandas": [ + [ + "BAYSOR_ESTIMATE_SCALE_FACTOR", + "pandas", + "3.0.3" + ] + ], + "versions_python": [ + [ + "BAYSOR_ESTIMATE_SCALE_FACTOR", + "python3", + "3.14.6" + ] + ] + } + ], + "timestamp": "2026-07-12T21:46:31.820728258", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.3" + } + } +} \ No newline at end of file diff --git a/modules/local/utility/parquet_to_csv/main.nf b/modules/local/utility/parquet2csv/main.nf similarity index 79% rename from modules/local/utility/parquet_to_csv/main.nf rename to modules/local/utility/parquet2csv/main.nf index 865408bc..8d741504 100644 --- a/modules/local/utility/parquet_to_csv/main.nf +++ b/modules/local/utility/parquet2csv/main.nf @@ -1,4 +1,4 @@ -process PARQUET_TO_CSV { +process PARQUET2CSV { tag "$meta.id" label 'process_low' @@ -8,7 +8,6 @@ process PARQUET_TO_CSV { input: tuple val(meta), path(transcripts) - val(extension) output: tuple val(meta), path("${prefix}/*.csv*"), emit: transcripts_csv @@ -20,21 +19,20 @@ process PARQUET_TO_CSV { script: // Exit if running this module with -profile conda / -profile mamba if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { - error "PARQUET_TO_CSV module does not support Conda. Please use Docker / Singularity / Podman instead." + error "PARQUET2CSV module does not support Conda. Please use Docker / Singularity / Podman instead." } prefix = task.ext.prefix ?: "${meta.id}" """ - utility_parquet_to_csv.py \\ + utility_parquet2csv.py \\ --transcripts ${transcripts} \\ - --extension ${extension} \\ --prefix ${prefix} """ stub: // Exit if running this module with -profile conda / -profile mamba if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { - error "PARQUET_TO_CSV module does not support Conda. Please use Docker / Singularity / Podman instead." + error "PARQUET2CSV module does not support Conda. Please use Docker / Singularity / Podman instead." } prefix = task.ext.prefix ?: "${meta.id}" diff --git a/modules/local/utility/parquet_to_csv/meta.yml b/modules/local/utility/parquet2csv/meta.yml similarity index 96% rename from modules/local/utility/parquet_to_csv/meta.yml rename to modules/local/utility/parquet2csv/meta.yml index 60d58fcc..de62154a 100644 --- a/modules/local/utility/parquet_to_csv/meta.yml +++ b/modules/local/utility/parquet2csv/meta.yml @@ -1,9 +1,9 @@ -name: "parquet_to_csv" +name: "parquet2csv" description: Tool suite for spatial omics data conversions. keywords: - xenium tools: - - "parquet_to_csv": + - "parquet2csv": description: "Collects functions to convert data formats for various types of data processing and analysis for spatial omics data." homepage: "https://github.com/heylf/spatialconverter" documentation: "https://github.com/heylf/spatialconverter" diff --git a/modules/local/baysor/preprocess/main.nf b/modules/local/utility/preprocess/main.nf similarity index 98% rename from modules/local/baysor/preprocess/main.nf rename to modules/local/utility/preprocess/main.nf index cfe6fe3b..459c01c1 100644 --- a/modules/local/baysor/preprocess/main.nf +++ b/modules/local/utility/preprocess/main.nf @@ -15,7 +15,7 @@ process BAYSOR_PREPROCESS_TRANSCRIPTS { val min_y output: - tuple val(meta), path("${prefix}/filtered_transcripts.csv"), emit: transcripts_file + tuple val(meta), path("${prefix}/filtered_transcripts.csv"), emit: transcripts_csv tuple val("${task.process}"), val('python'), eval("python3 --version | sed 's/Python //'"), topic: versions, emit: versions_python when: diff --git a/modules/local/baysor/preprocess/meta.yml b/modules/local/utility/preprocess/meta.yml similarity index 100% rename from modules/local/baysor/preprocess/meta.yml rename to modules/local/utility/preprocess/meta.yml diff --git a/modules/local/baysor/preprocess/tests/main.nf.test b/modules/local/utility/preprocess/tests/main.nf.test similarity index 100% rename from modules/local/baysor/preprocess/tests/main.nf.test rename to modules/local/utility/preprocess/tests/main.nf.test diff --git a/modules/local/baysor/preprocess/tests/main.nf.test.snap b/modules/local/utility/preprocess/tests/main.nf.test.snap similarity index 100% rename from modules/local/baysor/preprocess/tests/main.nf.test.snap rename to modules/local/utility/preprocess/tests/main.nf.test.snap diff --git a/modules/local/baysor/preprocess/tests/nextflow.config b/modules/local/utility/preprocess/tests/nextflow.config similarity index 100% rename from modules/local/baysor/preprocess/tests/nextflow.config rename to modules/local/utility/preprocess/tests/nextflow.config diff --git a/modules/nf-core/baysor/preview/environment.yml b/modules/nf-core/baysor/preview/environment.yml new file mode 100644 index 00000000..1030cd87 --- /dev/null +++ b/modules/nf-core/baysor/preview/environment.yml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - bioconda::baysor=0.7.1=*_2 diff --git a/modules/nf-core/baysor/preview/main.nf b/modules/nf-core/baysor/preview/main.nf new file mode 100644 index 00000000..3f4ffa1f --- /dev/null +++ b/modules/nf-core/baysor/preview/main.nf @@ -0,0 +1,39 @@ +process BAYSOR_PREVIEW { + tag "${meta.id}" + label 'process_medium' + + conda "${moduleDir}/environment.yml" + container "${workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/f7/f716efdfa817ee57bf59b78248f672b503807bfb8608ad3d3976f2e706ca9fb4/data' : + 'community.wave.seqera.io/library/baysor:0.7.1--6fd896e03359bae6'}" + + input: + tuple val(meta), path(transcripts), path(config) + + output: + tuple val(meta), path("${prefix}_preview.html"), emit: html + tuple val("${task.process}"), val('baysor'), eval("baysor --version"), topic: versions, emit: versions_baysor + + when: + task.ext.when == null || task.ext.when + + script: + def args = task.ext.args ?: '' + prefix = task.ext.prefix ?: "${meta.id}" + """ + export JULIA_NUM_THREADS=${task.cpus} + + baysor \\ + preview \\ + ${transcripts} \\ + --config ${config} \\ + --output ${prefix}_preview.html \\ + ${args} + """ + + stub: + prefix = task.ext.prefix ?: "${meta.id}" + """ + touch "${prefix}_preview.html" + """ +} \ No newline at end of file diff --git a/modules/nf-core/baysor/preview/meta.yml b/modules/nf-core/baysor/preview/meta.yml new file mode 100644 index 00000000..b66b44db --- /dev/null +++ b/modules/nf-core/baysor/preview/meta.yml @@ -0,0 +1,77 @@ +name: "baysor_preview" +description: Extract neighborhood composition vectors (NVCs) from a dataset. +keywords: + - preview + - preview_dataset + - preview_html +tools: + - "baysor": + description: "Baysor is a tool that segments cells using spatial gene expression + maps. Optionally, segmentation masks can be given as additional input." + homepage: "https://kharchenkolab.github.io/Baysor/dev/" + documentation: "https://kharchenkolab.github.io/Baysor/dev/" + tool_dev_url: "https://github.com/kharchenkolab/Baysor" + doi: "10.1038/s41587-021-01044-w" + licence: + - "MIT license" + identifier: "" +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test' ] + - transcripts: + type: file + description: | + Tab-delimited file with transcript information. Must contain the following columns: + - x: x-coordinate of the transcript + - y: y-coordinate of the transcript + - gene: gene name of the transcript + pattern: "transcripts.tsv" + ontologies: + - edam: http://edamontology.org/format_3475 + - config: + type: file + description: | + YAML configuration file for Baysor. Must contain the following fields: + - baysor_config: Baysor configuration parameters (see Baysor documentation for details) + pattern: "xenium.toml" + ontologies: [] +output: + html: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test' ] + - ${prefix}_preview.html: + type: file + description: HTML file with the preview of the Baysor segmentation + ontologies: + - edam: http://edamontology.org/format_2330 + versions_baysor: + - - ${task.process}: + type: string + description: The name of the process + - baysor: + type: string + description: The name of the tool + - baysor --version: + type: eval + description: The expression to obtain the version of the tool +topics: + versions: + - - ${task.process}: + type: string + description: The name of the process + - baysor: + type: string + description: The name of the tool + - baysor --version: + type: eval + description: The expression to obtain the version of the tool +authors: + - "@khersameesh24" +maintainers: + - "@khersameesh24" diff --git a/modules/nf-core/baysor/preview/tests/main.nf.test b/modules/nf-core/baysor/preview/tests/main.nf.test new file mode 100644 index 00000000..2a30382b --- /dev/null +++ b/modules/nf-core/baysor/preview/tests/main.nf.test @@ -0,0 +1,58 @@ +nextflow_process { + + name "Test Process BAYSOR PREVIEW" + script "../main.nf" + process "BAYSOR_PREVIEW" + + tag "modules" + tag "modules_nfcore" + tag "baysor" + tag "baysor/preview" + tag "preview" + + test("baysor preview - transcripts.parquet") { + + when { + process { + """ + input[0] = [ + [id: "test_run_baysor"], + file(params.modules_testdata_base_path + 'spatial_omics/xenium/homo_sapiens/transcripts.parquet', checkIfExists: true), + file(params.modules_testdata_base_path + 'spatial_omics/xenium/homo_sapiens/xenium.toml', checkIfExists: true) + ] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(sanitizeOutput(process.out, unstableKeys: ["html"])).match() } + ) + } + } + + test("baysor preview stub") { + + options "-stub" + + when { + process { + """ + input[0] = channel.of([ + [id: "test_run_baysor"], + file(params.modules_testdata_base_path + 'spatial_omics/xenium/homo_sapiens/transcripts.parquet', checkIfExists: true), + file(params.modules_testdata_base_path + 'spatial_omics/xenium/homo_sapiens/xenium.toml', checkIfExists: true) + ]) + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(sanitizeOutput(process.out)).match() } + ) + } + } +} \ No newline at end of file diff --git a/modules/nf-core/baysor/preview/tests/main.nf.test.snap b/modules/nf-core/baysor/preview/tests/main.nf.test.snap new file mode 100644 index 00000000..73b2fe40 --- /dev/null +++ b/modules/nf-core/baysor/preview/tests/main.nf.test.snap @@ -0,0 +1,54 @@ +{ + "baysor preview - transcripts.parquet": { + "content": [ + { + "html": [ + [ + { + "id": "test_run_baysor" + }, + "test_run_baysor_preview.html" + ] + ], + "versions_baysor": [ + [ + "BAYSOR_PREVIEW", + "baysor", + "0.7.1" + ] + ] + } + ], + "timestamp": "2026-06-04T17:52:07.624143739", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.3" + } + }, + "baysor preview stub": { + "content": [ + { + "html": [ + [ + { + "id": "test_run_baysor" + }, + "test_run_baysor_preview.html:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "versions_baysor": [ + [ + "BAYSOR_PREVIEW", + "baysor", + "0.7.1" + ] + ] + } + ], + "timestamp": "2026-06-04T17:52:19.690004063", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.3" + } + } +} \ No newline at end of file diff --git a/modules/nf-core/baysor/run/environment.yml b/modules/nf-core/baysor/run/environment.yml new file mode 100644 index 00000000..1030cd87 --- /dev/null +++ b/modules/nf-core/baysor/run/environment.yml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - bioconda::baysor=0.7.1=*_2 diff --git a/modules/nf-core/baysor/run/main.nf b/modules/nf-core/baysor/run/main.nf new file mode 100644 index 00000000..d0714859 --- /dev/null +++ b/modules/nf-core/baysor/run/main.nf @@ -0,0 +1,60 @@ +process BAYSOR_RUN { + tag "${meta.id}" + label 'process_high' + + conda "${moduleDir}/environment.yml" + container "${workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/f7/f716efdfa817ee57bf59b78248f672b503807bfb8608ad3d3976f2e706ca9fb4/data' : + 'community.wave.seqera.io/library/baysor:0.7.1--6fd896e03359bae6'}" + + input: + tuple val(meta), path(transcripts), path(prior_segmentation), path(config), val(scale) + val(prior_confidence) + val(prior_column) + val(polygon_format) + + output: + tuple val(meta), path("${prefix}_segmentation.csv"), path("${prefix}_segmentation_polygons_2d.json"), emit: segmentation + tuple val("${task.process}"), val('baysor'), eval("baysor --version"), topic: versions, emit: versions_baysor + + when: + task.ext.when == null || task.ext.when + + script: + def args = task.ext.args ?: '' + prefix = task.ext.prefix ?: "${meta.id}" + + def prior_seg = prior_column ? ":${prior_column}" : (prior_segmentation ?: '') + def confidence = prior_confidence ? "--prior-segmentation-confidence=${prior_confidence}" : '' + def scaling_factor = scale ? "--scale=${scale}" : '' + def config_arg = config ? "--config=${config}" : '' + + // check for valid output polygon format + def valid_formats = ['GeometryCollectionLegacy', 'GeometryCollection', 'FeatureCollection'] + if (!polygon_format in valid_formats) { + error "Invalid output polygon format. Valid options: ${valid_formats.join(', ')}" + } + def polygon_fmt = polygon_format ? "--polygon-format=${polygon_format}" : '--polygon-format=FeatureCollection' + + """ + export JULIA_NUM_THREADS=${task.cpus} + + baysor \\ + run \\ + ${transcripts} \\ + ${prior_seg} \\ + ${scaling_factor} \\ + ${confidence} \\ + --output ${prefix}_segmentation.csv \\ + ${config_arg} \\ + ${polygon_fmt} \\ + ${args} + """ + + stub: + prefix = task.ext.prefix ?: "${meta.id}" + """ + touch "${prefix}_segmentation.csv" + touch "${prefix}_segmentation_polygons_2d.json" + """ +} \ No newline at end of file diff --git a/modules/nf-core/baysor/run/meta.yml b/modules/nf-core/baysor/run/meta.yml new file mode 100644 index 00000000..92b28749 --- /dev/null +++ b/modules/nf-core/baysor/run/meta.yml @@ -0,0 +1,104 @@ +name: "baysor_run" +description: Bayesian segmentation of spatial transcriptomics data. +keywords: + - segmentation + - spatial transcriptomics + - cell clustering + - imaging +tools: + - "baysor": + description: "Baysor is a tool that segments cells using spatial gene expression + maps. Optionally, segmentation masks can be given as additional input." + homepage: "https://kharchenkolab.github.io/Baysor/dev/" + documentation: "https://kharchenkolab.github.io/Baysor/dev/" + tool_dev_url: "https://github.com/kharchenkolab/Baysor" + doi: "10.1038/s41587-021-01044-w" + licence: + - "MIT license" + identifier: "" +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test' ] + - transcripts: + type: file + description: CSV file + pattern: "*.csv" + ontologies: + - edam: http://edamontology.org/format_3752 + - prior_segmentation: + type: file + description: CSV file with segmentation mask (optional) + pattern: "*_segmentation.csv" + ontologies: + - edam: http://edamontology.org/format_3752 + - config: + type: file + description: TOML file with config arguments + pattern: "*.toml" + ontologies: [] + - scale: + type: float + description: Scale factor for the segmentation + - prior_confidence: + type: float + description: Confidence of the prior segmentation (optional) + - prior_column: + type: string + description: Column name in the prior segmentation file that contains the + segmentation mask (optional) + - polygon_format: + type: string + description: | + Format for output segmentation polygons (default: FeatureCollection) + enum: + - GeometryCollectionLegacy + - GeometryCollection + - FeatureCollection +output: + segmentation: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test' ] + - ${prefix}_segmentation.csv: + type: file + description: CSV file with segmentation mask + pattern: "*_segmentation.csv" + ontologies: + - edam: http://edamontology.org/format_3752 + - ${prefix}_segmentation_polygons_2d.json: + type: file + description: JSON file with segmentation polygons + pattern: "*_segmentation_polygons_2d.json" + ontologies: + - edam: http://edamontology.org/format_3752 + - edam: http://edamontology.org/format_3464 + versions_baysor: + - - ${task.process}: + type: string + description: The name of the process + - baysor: + type: string + description: The name of the tool + - baysor --version: + type: eval + description: The expression to obtain the version of the tool +topics: + versions: + - - ${task.process}: + type: string + description: The name of the process + - baysor: + type: string + description: The name of the tool + - baysor --version: + type: eval + description: The expression to obtain the version of the tool +authors: + - "@khersameesh24" +maintainers: + - "@khersameesh24" diff --git a/modules/local/baysor/run/tests/main.nf.test b/modules/nf-core/baysor/run/tests/main.nf.test similarity index 50% rename from modules/local/baysor/run/tests/main.nf.test rename to modules/nf-core/baysor/run/tests/main.nf.test index 37cda127..61e2617c 100644 --- a/modules/local/baysor/run/tests/main.nf.test +++ b/modules/nf-core/baysor/run/tests/main.nf.test @@ -5,7 +5,7 @@ nextflow_process { process "BAYSOR_RUN" tag "modules" - tag "modules_local" + tag "modules_nfcore" tag "baysor" tag "baysor/run" tag "segmentation" @@ -18,11 +18,14 @@ nextflow_process { """ input[0] = channel.of([ [id: "test_run_baysor"], - file("https://raw.githubusercontent.com/khersameesh24/test-datasets/baysor/transcripts.parquet", checkIfExists: true), + file(params.modules_testdata_base_path + 'spatial_omics/xenium/homo_sapiens/transcripts.parquet', checkIfExists: true), [], - file("https://raw.githubusercontent.com/khersameesh24/test-datasets/baysor/config/xenium.toml", checkIfExists: true), + file(params.modules_testdata_base_path + 'spatial_omics/xenium/homo_sapiens/xenium.toml', checkIfExists: true), 30 ]) + input[1] = 0.5 + input[2] = "cell_id" + input[3] = "GeometryCollectionLegacy" """ } } @@ -30,11 +33,7 @@ nextflow_process { then { assertAll( { assert process.success }, - { assert file(process.out.segmentation[0][1]).exists() }, - { assert file(process.out.segmentation[0][1]).name == "segmentation.csv" }, - { assert file(process.out.segmentation[0][2]).exists() }, - { assert file(process.out.segmentation[0][2]).name == "segmentation_polygons_2d.json" }, - { assert snapshot(process.out.versions_baysor).match("versions") } + { assert snapshot(sanitizeOutput(process.out, unstableKeys: ["segmentation"])).match() } ) } } @@ -48,11 +47,14 @@ nextflow_process { """ input[0] = channel.of([ [id: "test_run_baysor"], - file("https://raw.githubusercontent.com/khersameesh24/test-datasets/baysor/transcripts.parquet", checkIfExists: true), + file(params.modules_testdata_base_path + 'spatial_omics/xenium/homo_sapiens/transcripts.parquet', checkIfExists: true), [], - file("https://raw.githubusercontent.com/khersameesh24/test-datasets/baysor/config/xenium.toml", checkIfExists: true), + file(params.modules_testdata_base_path + 'spatial_omics/xenium/homo_sapiens/xenium.toml', checkIfExists: true), 30 ]) + input[1] = 0.5 + input[2] = "cell_id" + input[3] = "GeometryCollectionLegacy" """ } } @@ -60,8 +62,8 @@ nextflow_process { then { assertAll( { assert process.success }, - { assert snapshot(process.out.versions_baysor).match("versions_stub") } + { assert snapshot(sanitizeOutput(process.out)).match() } ) } } -} +} \ No newline at end of file diff --git a/modules/nf-core/baysor/run/tests/main.nf.test.snap b/modules/nf-core/baysor/run/tests/main.nf.test.snap new file mode 100644 index 00000000..8c74aaac --- /dev/null +++ b/modules/nf-core/baysor/run/tests/main.nf.test.snap @@ -0,0 +1,56 @@ +{ + "baysor run - transcripts.parquet": { + "content": [ + { + "segmentation": [ + [ + { + "id": "test_run_baysor" + }, + "test_run_baysor_segmentation.csv", + "test_run_baysor_segmentation_polygons_2d.json" + ] + ], + "versions_baysor": [ + [ + "BAYSOR_RUN", + "baysor", + "0.7.1" + ] + ] + } + ], + "timestamp": "2026-06-04T18:01:04.787324737", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.3" + } + }, + "baysor run stub": { + "content": [ + { + "segmentation": [ + [ + { + "id": "test_run_baysor" + }, + "test_run_baysor_segmentation.csv:md5,d41d8cd98f00b204e9800998ecf8427e", + "test_run_baysor_segmentation_polygons_2d.json:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "versions_baysor": [ + [ + "BAYSOR_RUN", + "baysor", + "0.7.1" + ] + ] + } + ], + "timestamp": "2026-06-04T17:52:35.702288758", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.3" + } + } +} \ No newline at end of file diff --git a/modules/nf-core/baysor/segfree/environment.yml b/modules/nf-core/baysor/segfree/environment.yml new file mode 100644 index 00000000..1030cd87 --- /dev/null +++ b/modules/nf-core/baysor/segfree/environment.yml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - bioconda::baysor=0.7.1=*_2 diff --git a/modules/nf-core/baysor/segfree/main.nf b/modules/nf-core/baysor/segfree/main.nf new file mode 100644 index 00000000..1587c046 --- /dev/null +++ b/modules/nf-core/baysor/segfree/main.nf @@ -0,0 +1,40 @@ +process BAYSOR_SEGFREE { + tag "${meta.id}" + label 'process_high' + + conda "${moduleDir}/environment.yml" + container "${workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/f7/f716efdfa817ee57bf59b78248f672b503807bfb8608ad3d3976f2e706ca9fb4/data' : + 'community.wave.seqera.io/library/baysor:0.7.1--6fd896e03359bae6'}" + + input: + tuple val(meta), path(transcripts), path(config) + + output: + tuple val(meta), path("${prefix}_ncvs.loom"), emit: ncvs + tuple val("${task.process}"), val('baysor'), eval("baysor --version"), topic: versions, emit: versions_baysor + + when: + task.ext.when == null || task.ext.when + + script: + def args = task.ext.args ?: '' + prefix = task.ext.prefix ?: "${meta.id}" + + """ + export JULIA_NUM_THREADS=${task.cpus} + + baysor \\ + segfree \\ + ${transcripts} \\ + --config ${config} \\ + --output ${prefix}_ncvs.loom \\ + ${args} + """ + + stub: + prefix = task.ext.prefix ?: "${meta.id}" + """ + touch "${prefix}_ncvs.loom" + """ +} \ No newline at end of file diff --git a/modules/nf-core/baysor/segfree/meta.yml b/modules/nf-core/baysor/segfree/meta.yml new file mode 100644 index 00000000..59fe536c --- /dev/null +++ b/modules/nf-core/baysor/segfree/meta.yml @@ -0,0 +1,78 @@ +name: "baysor_segfree" +description: Extract neighborhood composition vectors (NVCs) from a dataset. +keywords: + - neighborhood + - segmentation-free + - ncvs-looms +tools: + - "baysor": + description: "Baysor is a tool that segments cells using spatial gene expression + maps. Optionally, segmentation masks can be given as additional input." + homepage: "https://kharchenkolab.github.io/Baysor/dev/" + documentation: "https://kharchenkolab.github.io/Baysor/dev/" + tool_dev_url: "https://github.com/kharchenkolab/Baysor" + doi: "10.1038/s41587-021-01044-w" + licence: + - "MIT license" + identifier: "" +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test' ] + - transcripts: + type: file + description: | + Tab-delimited file with transcript information. Must contain the following columns: + - x: x-coordinate of the transcript + - y: y-coordinate of the transcript + - gene: gene name of the transcript + pattern: "transcripts.tsv" + ontologies: + - edam: http://edamontology.org/format_3475 + - config: + type: file + description: | + YAML configuration file for Baysor. Must contain the following fields: + - baysor_config: Baysor configuration parameters (see Baysor documentation for details) + pattern: "xenium.toml" + ontologies: [] +output: + ncvs: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test' ] + - ${prefix}_ncvs.loom: + type: file + description: Loom file containing neighborhood composition vectors + (NVCs) + ontologies: + - edam: http://edamontology.org/format_3602 + versions_baysor: + - - ${task.process}: + type: string + description: The name of the process + - baysor: + type: string + description: The name of the tool + - baysor --version: + type: eval + description: The expression to obtain the version of the tool +topics: + versions: + - - ${task.process}: + type: string + description: The name of the process + - baysor: + type: string + description: The name of the tool + - baysor --version: + type: eval + description: The expression to obtain the version of the tool +authors: + - "@khersameesh24" +maintainers: + - "@khersameesh24" diff --git a/modules/nf-core/baysor/segfree/tests/main.nf.test b/modules/nf-core/baysor/segfree/tests/main.nf.test new file mode 100644 index 00000000..2ca98e26 --- /dev/null +++ b/modules/nf-core/baysor/segfree/tests/main.nf.test @@ -0,0 +1,58 @@ +nextflow_process { + + name "Test Process BAYSOR SEGFREE" + script "../main.nf" + process "BAYSOR_SEGFREE" + + tag "modules" + tag "modules_nfcore" + tag "baysor" + tag "baysor/segfree" + tag "segmentation-free" + + test("baysor segfree - transcripts.parquet") { + + when { + process { + """ + input[0] = channel.of([ + [id: "test_run_baysor"], + file(params.modules_testdata_base_path + 'spatial_omics/xenium/homo_sapiens/transcripts.parquet', checkIfExists: true), + file(params.modules_testdata_base_path + 'spatial_omics/xenium/homo_sapiens/xenium.toml', checkIfExists: true) + ]) + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(sanitizeOutput(process.out, unstableKeys: ["ncvs"])).match() } + ) + } + } + + test("baysor segfree stub") { + + options "-stub" + + when { + process { + """ + input[0] = channel.of([ + [id: "test_run_baysor"], + file(params.modules_testdata_base_path + 'spatial_omics/xenium/homo_sapiens/transcripts.parquet', checkIfExists: true), + file(params.modules_testdata_base_path + 'spatial_omics/xenium/homo_sapiens/xenium.toml', checkIfExists: true) + ]) + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(sanitizeOutput(process.out)).match() } + ) + } + } +} \ No newline at end of file diff --git a/modules/nf-core/baysor/segfree/tests/main.nf.test.snap b/modules/nf-core/baysor/segfree/tests/main.nf.test.snap new file mode 100644 index 00000000..5fde7958 --- /dev/null +++ b/modules/nf-core/baysor/segfree/tests/main.nf.test.snap @@ -0,0 +1,54 @@ +{ + "baysor segfree - transcripts.parquet": { + "content": [ + { + "ncvs": [ + [ + { + "id": "test_run_baysor" + }, + "test_run_baysor_ncvs.loom" + ] + ], + "versions_baysor": [ + [ + "BAYSOR_SEGFREE", + "baysor", + "0.7.1" + ] + ] + } + ], + "timestamp": "2026-06-04T17:53:45.874991494", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.3" + } + }, + "baysor segfree stub": { + "content": [ + { + "ncvs": [ + [ + { + "id": "test_run_baysor" + }, + "test_run_baysor_ncvs.loom:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "versions_baysor": [ + [ + "BAYSOR_SEGFREE", + "baysor", + "0.7.1" + ] + ] + } + ], + "timestamp": "2026-06-04T17:53:58.139697288", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.3" + } + } +} \ No newline at end of file diff --git a/modules/nf-core/opt/flip/environment.yml b/modules/nf-core/opt/flip/environment.yml new file mode 100644 index 00000000..32098a56 --- /dev/null +++ b/modules/nf-core/opt/flip/environment.yml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - "bioconda::opt=0.0.1" diff --git a/modules/nf-core/opt/flip/main.nf b/modules/nf-core/opt/flip/main.nf index 66be07d0..6f419db7 100644 --- a/modules/nf-core/opt/flip/main.nf +++ b/modules/nf-core/opt/flip/main.nf @@ -2,7 +2,10 @@ process OPT_FLIP { tag "$meta.id" label 'process_high' - container "khersameesh24/opt:v0.0.1" + conda "${moduleDir}/environment.yml" + container "${workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/64/64550ef193f98ea294d70e087a80e37bbf0c5c4a5920f0a22414ed8a11c32caa/data' : + 'community.wave.seqera.io/library/opt:0.0.1--f0b1e63f50e38ab1'}" input: tuple val(meta), path(probes_fasta) @@ -10,51 +13,31 @@ process OPT_FLIP { output: tuple val(meta), path("${prefix}/fwd_oriented.fa"), emit: fwd_oriented_fa - path "versions.yml" , emit: versions + tuple val("${task.process}"), val('opt'), eval("opt --version"), topic: versions, emit: versions_opt when: task.ext.when == null || task.ext.when script: - // Exit if running this module with -profile conda / -profile mamba - if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { - error "OPT_FLIP module does not support Conda. Please use Docker / Singularity / Podman instead." - } - def args = task.ext.args ?: '' prefix = task.ext.prefix ?: "${meta.id}" - + """ opt \\ - -o ${prefix} \\ - -p ${task.cpus} \\ - flip \\ - -q ${probes_fasta} \\ - -a ${ref_annot_gff} \\ - -t ${ref_annot_fa} \\ - ${args} - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - opt: \$(opt --version) - END_VERSIONS + -o ${prefix} \\ + -p ${task.cpus} \\ + flip \\ + -i ${probes_fasta} \\ + -a ${ref_annot_gff} \\ + -f ${ref_annot_fa} \\ + ${args} """ stub: - // Exit if running this module with -profile conda / -profile mamba - if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { - error "OPT_FLIP module does not support Conda. Please use Docker / Singularity / Podman instead." - } - prefix = task.ext.prefix ?: "${meta.id}" - + """ mkdir -p ${prefix} touch "${prefix}/fwd_oriented.fa" - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - opt: \$(opt --version) - END_VERSIONS """ } diff --git a/modules/nf-core/opt/flip/meta.yml b/modules/nf-core/opt/flip/meta.yml index f0e4c57c..280a5e01 100644 --- a/modules/nf-core/opt/flip/meta.yml +++ b/modules/nf-core/opt/flip/meta.yml @@ -1,6 +1,6 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/meta-schema.json name: "opt_flip" -description: "flip corrects probes that are aligning to the opposite strand of their intended target genes by reverse complementing them" +description: "flip corrects probes that are aligning to the opposite strand of their + intended target genes by reverse complementing them" keywords: - opt - opt flip @@ -9,12 +9,14 @@ keywords: - align probes tools: - "opt": - description: "opt is a simple program that aligns probe sequences to transcript sequences to detect potential off-target probe activity" + description: "opt is a simple program that aligns probe sequences to transcript + sequences to detect potential off-target probe activity" homepage: "https://github.com/JEFworks-Lab/off-target-probe-tracker" documentation: "https://github.com/JEFworks-Lab/off-target-probe-tracker/blob/main/README.md" tool_dev_url: "https://github.com/JEFworks-Lab/off-target-probe-tracker" - licence: [GPL-3.0 license] - + licence: + - GPL-3.0 license + identifier: "" input: - - meta: type: map @@ -23,7 +25,8 @@ input: e.g. `[ id:'breast_cancer_probe_panel_sequences' ]` - probes_fasta: type: file - description: Fasta file for the probe sequences used in the xenium experiment + description: Fasta file for the probe sequences used in the xenium + experiment pattern: "*.fasta" ontologies: [] - - meta2: @@ -41,7 +44,6 @@ input: description: Reference annotations in fasta format pattern: "*.fa" ontologies: [] - output: fwd_oriented_fa: - - meta: @@ -49,19 +51,34 @@ output: description: | Groovy Map containing information of the forward oriented fasta generated with the probes panel sequences 'opt flip' e.g. `[ id:'breast_cancer_probe_panel_sequences' ]` - - "${meta.id}/fwd_oriented.fa": + - ${prefix}/fwd_oriented.fa: type: file - description: The forward oriented fasta file - pattern: "*.fa" - ontologies: [] + description: Fasta file for the forward oriented probe sequences + generated with 'opt flip' + pattern: "*fwd_oriented.fa" + ontologies: + - edam: http://edamontology.org/format_1929 + versions_opt: + - - ${task.process}: + type: string + description: The name of the process + - opt: + type: string + description: The name of the tool + - opt --version: + type: eval + description: The expression to obtain the version of the tool +topics: versions: - - "versions.yml": - type: file - description: File containing software versions - pattern: "versions.yml" - ontologies: - - edam: "http://edamontology.org/format_3750" # YAML - + - - ${task.process}: + type: string + description: The name of the process + - opt: + type: string + description: The name of the tool + - opt --version: + type: eval + description: The expression to obtain the version of the tool authors: - "@khersameesh24" maintainers: diff --git a/modules/nf-core/opt/flip/opt-flip.diff b/modules/nf-core/opt/flip/opt-flip.diff deleted file mode 100644 index 09c6098d..00000000 --- a/modules/nf-core/opt/flip/opt-flip.diff +++ /dev/null @@ -1,48 +0,0 @@ -Changes in component 'nf-core/opt/flip' -Changes in 'opt/flip/main.nf': ---- modules/nf-core/opt/flip/main.nf -+++ modules/nf-core/opt/flip/main.nf -@@ -9,8 +9,8 @@ - tuple val(meta2), path(ref_annot_gff), path(ref_annot_fa) - - output: -- tuple val(meta), path("${meta.id}/fwd_oriented.fa"), emit: fwd_oriented_fa -- path "versions.yml" , emit: versions -+ tuple val(meta), path("${prefix}/fwd_oriented.fa"), emit: fwd_oriented_fa -+ path "versions.yml" , emit: versions - - when: - task.ext.when == null || task.ext.when -@@ -20,9 +20,10 @@ - if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { - error "OPT_FLIP module does not support Conda. Please use Docker / Singularity / Podman instead." - } -- def args = task.ext.args ?: '' -- def prefix = task.ext.prefix ?: "${meta.id}" - -+ def args = task.ext.args ?: '' -+ prefix = task.ext.prefix ?: "${meta.id}" -+ - """ - opt \\ - -o ${prefix} \\ -@@ -40,8 +41,13 @@ - """ - - stub: -- def prefix = task.ext.prefix ?: "${meta.id}" -+ // Exit if running this module with -profile conda / -profile mamba -+ if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { -+ error "OPT_FLIP module does not support Conda. Please use Docker / Singularity / Podman instead." -+ } - -+ prefix = task.ext.prefix ?: "${meta.id}" -+ - """ - mkdir -p ${prefix} - touch "${prefix}/fwd_oriented.fa" - -'modules/nf-core/opt/flip/meta.yml' is unchanged -'modules/nf-core/opt/flip/tests/main.nf.test' is unchanged -'modules/nf-core/opt/flip/tests/main.nf.test.snap' is unchanged -************************************************************ diff --git a/modules/nf-core/opt/flip/tests/main.nf.test b/modules/nf-core/opt/flip/tests/main.nf.test index 77fd9ef4..c81b08c4 100644 --- a/modules/nf-core/opt/flip/tests/main.nf.test +++ b/modules/nf-core/opt/flip/tests/main.nf.test @@ -9,52 +9,56 @@ nextflow_process { tag "opt" tag "opt/flip" - test("testrun panel probe sequences") { + test("testrun opt/flip panel probe sequences") { when { process { """ input[0] = [ - [ id:'test_run' ], - file('https://raw.githubusercontent.com/khersameesh24/test-datasets/opt/testdata/panel_probe_sequences.fasta', checkIfExists: true) + [ id:'testrun_opt_flip' ], + file(params.modules_testdata_base_path + 'spatial_omics/probe_detection/probes.fa', checkIfExists: true) ] input[1] = [ - [ id:'test_run' ], - file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.gtf', checkIfExists: true), - file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta', checkIfExists: true) + [ id:'test_references' ], + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/minigenome.gtf', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/minigenome.fa', checkIfExists: true) ] """ } } + then { assertAll( { assert process.success }, - { assert snapshot(process.out).match() } - ) + { assert snapshot(sanitizeOutput(process.out)).match() } + ) } } - test("testrun panel probe sequences -stub") { + test("testrun opt/flip panel probe sequences -stub") { + options "-stub" + when { process { """ input[0] = [ - [ id:'test_run' ], - file('https://raw.githubusercontent.com/khersameesh24/test-datasets/opt/testdata/panel_probe_sequences.fasta', checkIfExists: true) + [ id:'testrun_opt_stat' ], + file(params.modules_testdata_base_path + 'spatial_omics/probe_detection/probes.fa', checkIfExists: true) ] input[1] = [ - [ id:'test_run' ], - file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.gtf', checkIfExists: true), - file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta', checkIfExists: true) + [ id:'test_references' ], + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/minigenome.gtf', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/minigenome.fa', checkIfExists: true) ] """ } } + then { assertAll( { assert process.success }, - { assert snapshot(process.out).match() } + { assert snapshot(sanitizeOutput(process.out)).match() } ) } } diff --git a/modules/nf-core/opt/flip/tests/main.nf.test.snap b/modules/nf-core/opt/flip/tests/main.nf.test.snap index 4bc5b35e..cc60e73d 100644 --- a/modules/nf-core/opt/flip/tests/main.nf.test.snap +++ b/modules/nf-core/opt/flip/tests/main.nf.test.snap @@ -1,68 +1,54 @@ { - "testrun panel probe sequences -stub": { + "testrun opt/flip panel probe sequences -stub": { "content": [ { - "0": [ + "fwd_oriented_fa": [ [ { - "id": "test_run" + "id": "testrun_opt_stat" }, "fwd_oriented.fa:md5,d41d8cd98f00b204e9800998ecf8427e" ] ], - "1": [ - "versions.yml:md5,57dbc672cf0bf40854c18d241f2d7d2e" - ], - "fwd_oriented_fa": [ + "versions_opt": [ [ - { - "id": "test_run" - }, - "fwd_oriented.fa:md5,d41d8cd98f00b204e9800998ecf8427e" + "OPT_FLIP", + "opt", + "opt v0.0.1" ] - ], - "versions": [ - "versions.yml:md5,57dbc672cf0bf40854c18d241f2d7d2e" ] } ], + "timestamp": "2026-06-04T12:09:01.068451352", "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.6" - }, - "timestamp": "2025-09-14T12:43:08.105988801" + "nf-test": "0.9.5", + "nextflow": "26.04.3" + } }, - "testrun panel probe sequences": { + "testrun opt/flip panel probe sequences": { "content": [ { - "0": [ + "fwd_oriented_fa": [ [ { - "id": "test_run" + "id": "testrun_opt_flip" }, - "fwd_oriented.fa:md5,535289c04851ad94e091ec7c14ff6bcd" + "fwd_oriented.fa:md5,6e880a8dbc3a6cc998caaeacd35003fa" ] ], - "1": [ - "versions.yml:md5,57dbc672cf0bf40854c18d241f2d7d2e" - ], - "fwd_oriented_fa": [ + "versions_opt": [ [ - { - "id": "test_run" - }, - "fwd_oriented.fa:md5,535289c04851ad94e091ec7c14ff6bcd" + "OPT_FLIP", + "opt", + "opt v0.0.1" ] - ], - "versions": [ - "versions.yml:md5,57dbc672cf0bf40854c18d241f2d7d2e" ] } ], + "timestamp": "2026-06-04T12:08:46.969721441", "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.6" - }, - "timestamp": "2025-09-14T12:47:41.966183182" + "nf-test": "0.9.5", + "nextflow": "26.04.3" + } } } \ No newline at end of file diff --git a/modules/nf-core/opt/stat/environment.yml b/modules/nf-core/opt/stat/environment.yml new file mode 100644 index 00000000..32098a56 --- /dev/null +++ b/modules/nf-core/opt/stat/environment.yml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - "bioconda::opt=0.0.1" diff --git a/modules/nf-core/opt/stat/main.nf b/modules/nf-core/opt/stat/main.nf index e8de5860..af4099a3 100644 --- a/modules/nf-core/opt/stat/main.nf +++ b/modules/nf-core/opt/stat/main.nf @@ -2,7 +2,10 @@ process OPT_STAT { tag "$meta.id" label 'process_high' - container "khersameesh24/opt:v0.0.1" + conda "${moduleDir}/environment.yml" + container "${workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/64/64550ef193f98ea294d70e087a80e37bbf0c5c4a5920f0a22414ed8a11c32caa/data' : + 'community.wave.seqera.io/library/opt:0.0.1--f0b1e63f50e38ab1'}" input: tuple val(meta), path(probe_targets) @@ -11,50 +14,32 @@ process OPT_STAT { output: tuple val(meta), path("${prefix}/collapsed_summary.tsv"), emit: summary - path "versions.yml" , emit: versions + tuple val("${task.process}"), val('opt'), eval("opt --version"), topic: versions, emit: versions_opt when: task.ext.when == null || task.ext.when script: - // Exit if running this module with -profile conda / -profile mamba - if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { - error "OPT_STAT module does not support Conda. Please use Docker / Singularity / Podman instead." - } def args = task.ext.args ?: '' - def synonyms = gene_synonyms ? "-s ${gene_synonyms}": "" prefix = task.ext.prefix ?: "${meta.id}" + def synonyms = gene_synonyms ? "-s ${gene_synonyms}": "" + """ opt \\ - -o ${prefix} \\ - stat \\ - -i ${probe_targets} \\ - -q ${fwd_oriented_probes} \\ - ${synonyms} \\ - ${args} - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - opt: \$(opt --version) - END_VERSIONS + -o ${prefix} \\ + stat \\ + -i ${probe_targets} \\ + -q ${fwd_oriented_probes} \\ + ${synonyms} \\ + ${args} """ stub: - // Exit if running this module with -profile conda / -profile mamba - if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { - error "OPT_STAT module does not support Conda. Please use Docker / Singularity / Podman instead." - } - prefix = task.ext.prefix ?: "${meta.id}" """ mkdir -p ${prefix} touch "${prefix}/collapsed_summary.tsv" - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - opt: \$(opt --version) - END_VERSIONS """ } diff --git a/modules/nf-core/opt/stat/meta.yml b/modules/nf-core/opt/stat/meta.yml index ae2d712b..30188007 100644 --- a/modules/nf-core/opt/stat/meta.yml +++ b/modules/nf-core/opt/stat/meta.yml @@ -1,4 +1,3 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/meta-schema.json name: "opt_stat" description: "stat summarizes opt binding predictions" keywords: @@ -16,9 +15,9 @@ tools: homepage: "https://github.com/JEFworks-Lab/off-target-probe-tracker" documentation: "https://github.com/JEFworks-Lab/off-target-probe-tracker/blob/main/README.md" tool_dev_url: "https://github.com/JEFworks-Lab/off-target-probe-tracker" - licence: [GPL-3.0 license] + licence: + - GPL-3.0 license identifier: "" - input: - - meta: type: map @@ -29,7 +28,8 @@ input: type: file description: Generated probe targets pattern: "*.tsv" - ontologies: [] + ontologies: + - edam: http://edamontology.org/format_3475 - - meta2: type: map description: | @@ -45,7 +45,8 @@ input: description: Gene synonyms that may have been counted as off-targets but simply differ in name (optional input) pattern: "*.csv" - ontologies: [] + ontologies: + - edam: http://edamontology.org/format_3752 output: summary: - - meta: @@ -53,19 +54,33 @@ output: description: | Groovy Map containing summary of the forward oriented probes generated with the panel sequences 'opt flip and track' e.g. `[ id:'breast_cancer_probe_panel_sequences' ]` - - "${meta.id}/collapsed_summary.tsv": + - ${prefix}/collapsed_summary.tsv: type: file - description: tsv file containing the summary stats - pattern: "*.tsv" - ontologies: [] + description: Summary of the opt binding predictions + pattern: "*collapsed_summary.tsv" + ontologies: + - edam: http://edamontology.org/format_3475 + versions_opt: + - - ${task.process}: + type: string + description: The name of the process + - opt: + type: string + description: The name of the tool + - opt --version: + type: eval + description: The expression to obtain the version of the tool +topics: versions: - - versions.yml: - type: file - description: File containing software versions - pattern: "versions.yml" - ontologies: - - edam: "http://edamontology.org/format_3750" # YAML - + - - ${task.process}: + type: string + description: The name of the process + - opt: + type: string + description: The name of the tool + - opt --version: + type: eval + description: The expression to obtain the version of the tool authors: - "@khersameesh24" maintainers: diff --git a/modules/nf-core/opt/stat/opt-stat.diff b/modules/nf-core/opt/stat/opt-stat.diff deleted file mode 100644 index 348e77b5..00000000 --- a/modules/nf-core/opt/stat/opt-stat.diff +++ /dev/null @@ -1,44 +0,0 @@ -Changes in component 'nf-core/opt/stat' -Changes in 'opt/stat/main.nf': ---- modules/nf-core/opt/stat/main.nf -+++ modules/nf-core/opt/stat/main.nf -@@ -10,8 +10,8 @@ - path(gene_synonyms) - - output: -- tuple val(meta), path("${meta.id}/collapsed_summary.tsv"), emit: summary -- path "versions.yml" , emit: versions -+ tuple val(meta), path("${prefix}/collapsed_summary.tsv"), emit: summary -+ path "versions.yml" , emit: versions - - when: - task.ext.when == null || task.ext.when -@@ -22,8 +22,8 @@ - error "OPT_STAT module does not support Conda. Please use Docker / Singularity / Podman instead." - } - def args = task.ext.args ?: '' -- def prefix = task.ext.prefix ?: "${meta.id}" - def synonyms = gene_synonyms ? "-s ${gene_synonyms}": "" -+ prefix = task.ext.prefix ?: "${meta.id}" - - """ - opt \\ -@@ -41,7 +41,12 @@ - """ - - stub: -- def prefix = task.ext.prefix ?: "${meta.id}" -+ // Exit if running this module with -profile conda / -profile mamba -+ if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { -+ error "OPT_STAT module does not support Conda. Please use Docker / Singularity / Podman instead." -+ } -+ -+ prefix = task.ext.prefix ?: "${meta.id}" - - """ - mkdir -p ${prefix} - -'modules/nf-core/opt/stat/meta.yml' is unchanged -'modules/nf-core/opt/stat/tests/main.nf.test' is unchanged -'modules/nf-core/opt/stat/tests/main.nf.test.snap' is unchanged -************************************************************ diff --git a/modules/nf-core/opt/stat/tests/main.nf.test b/modules/nf-core/opt/stat/tests/main.nf.test index 5204a81f..6451ecb3 100644 --- a/modules/nf-core/opt/stat/tests/main.nf.test +++ b/modules/nf-core/opt/stat/tests/main.nf.test @@ -7,56 +7,82 @@ nextflow_process { tag "modules" tag "modules_nfcore" tag "opt" + tag "opt/flip" + tag "opt/track" tag "opt/stat" - test("testrun panel probe sequences") { - - when { + setup { + run("OPT_FLIP") { + script "../../flip/main.nf" process { """ input[0] = [ - [ id:'test_run' ], - file('https://raw.githubusercontent.com/khersameesh24/test-datasets/opt/testdata/probe2targets.tsv', checkIfExists: true) + [ id:'testrun_opt_stat' ], + file(params.modules_testdata_base_path + 'spatial_omics/probe_detection/probes.fa', checkIfExists: true) ] input[1] = [ - [ id:'test_run' ], - file('https://raw.githubusercontent.com/khersameesh24/test-datasets/opt/testdata/fwd_oriented.fa', checkIfExists: true) + [ id:'test_references' ], + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/minigenome.gtf', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/minigenome.fa', checkIfExists: true) ] - input[2] = [] - // input[2] = [file('https://raw.githubusercontent.com/khersameesh24/test-datasets/opt/testdata/gene_synonyms.csv', checkIfExists: true)] """ } } + + run("OPT_TRACK") { + script "../../track/main.nf" + process { + """ + input[0] = OPT_FLIP.out.fwd_oriented_fa + input[1] = [ + [ id:'test_references' ], + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/minigenome.gtf', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/minigenome.fa', checkIfExists: true) + ] + """ + } + } + } + + test("testrun opt/stat panel probe sequences") { + + when { + + process { + """ + input[0] = OPT_TRACK.out.probes2target + input[1] = OPT_FLIP.out.fwd_oriented_fa + input[2] = [file(params.modules_testdata_base_path + 'spatial_omics/probe_detection/gene_synonyms.csv', checkIfExists: true)] + """ + } + } + then { assertAll( { assert process.success }, - { assert snapshot(process.out).match() } + { assert snapshot(sanitizeOutput(process.out)).match() } ) } } - test("testrun panel probe sequences -stub") { + test("testrun opt/stat panel probe sequences -stub") { + options "-stub" + when { process { """ - input[0] = [ - [ id:'test_run' ], - file('https://raw.githubusercontent.com/khersameesh24/test-datasets/opt/testdata/probe2targets.tsv', checkIfExists: true) - ] - input[1] = [ - [ id:'test_run' ], - file('https://raw.githubusercontent.com/khersameesh24/test-datasets/opt/testdata/fwd_oriented.fa', checkIfExists: true) - ] - input[2] = [] - // input[2] = [file('https://raw.githubusercontent.com/khersameesh24/test-datasets/opt/testdata/gene_synonyms.csv', checkIfExists: true)] + input[0] = OPT_TRACK.out.probes2target + input[1] = OPT_FLIP.out.fwd_oriented_fa + input[2] = [file(params.modules_testdata_base_path + 'spatial_omics/probe_detection/gene_synonyms.csv', checkIfExists: true)] """ } } + then { assertAll( { assert process.success }, - { assert snapshot(process.out).match() } + { assert snapshot(sanitizeOutput(process.out)).match() } ) } } diff --git a/modules/nf-core/opt/stat/tests/main.nf.test.snap b/modules/nf-core/opt/stat/tests/main.nf.test.snap index b14671fe..74cef7f7 100644 --- a/modules/nf-core/opt/stat/tests/main.nf.test.snap +++ b/modules/nf-core/opt/stat/tests/main.nf.test.snap @@ -1,68 +1,54 @@ { - "testrun panel probe sequences -stub": { + "testrun opt/stat panel probe sequences": { "content": [ { - "0": [ + "summary": [ [ { - "id": "test_run" + "id": "testrun_opt_stat" }, - "collapsed_summary.tsv:md5,d41d8cd98f00b204e9800998ecf8427e" + "collapsed_summary.tsv:md5,b2884d9c8c89124d3cbbbc1223d81c99" ] ], - "1": [ - "versions.yml:md5,a23b08ea3b2da18863a5611dd0adbaa1" - ], - "summary": [ + "versions_opt": [ [ - { - "id": "test_run" - }, - "collapsed_summary.tsv:md5,d41d8cd98f00b204e9800998ecf8427e" + "OPT_STAT", + "opt", + "opt v0.0.1" ] - ], - "versions": [ - "versions.yml:md5,a23b08ea3b2da18863a5611dd0adbaa1" ] } ], + "timestamp": "2026-06-04T11:46:01.953301169", "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.6" - }, - "timestamp": "2025-09-14T12:23:32.959930982" + "nf-test": "0.9.5", + "nextflow": "26.04.3" + } }, - "testrun panel probe sequences": { + "testrun opt/stat panel probe sequences -stub": { "content": [ { - "0": [ + "summary": [ [ { - "id": "test_run" + "id": "testrun_opt_stat" }, - "collapsed_summary.tsv:md5,b2884d9c8c89124d3cbbbc1223d81c99" + "collapsed_summary.tsv:md5,d41d8cd98f00b204e9800998ecf8427e" ] ], - "1": [ - "versions.yml:md5,a23b08ea3b2da18863a5611dd0adbaa1" - ], - "summary": [ + "versions_opt": [ [ - { - "id": "test_run" - }, - "collapsed_summary.tsv:md5,b2884d9c8c89124d3cbbbc1223d81c99" + "OPT_STAT", + "opt", + "opt v0.0.1" ] - ], - "versions": [ - "versions.yml:md5,a23b08ea3b2da18863a5611dd0adbaa1" ] } ], + "timestamp": "2026-06-04T11:46:24.807423571", "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.6" - }, - "timestamp": "2025-09-14T12:58:57.17786325" + "nf-test": "0.9.5", + "nextflow": "26.04.3" + } } } \ No newline at end of file diff --git a/modules/nf-core/opt/track/environment.yml b/modules/nf-core/opt/track/environment.yml new file mode 100644 index 00000000..32098a56 --- /dev/null +++ b/modules/nf-core/opt/track/environment.yml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - "bioconda::opt=0.0.1" diff --git a/modules/nf-core/opt/track/main.nf b/modules/nf-core/opt/track/main.nf index ff92645e..f3d5146f 100644 --- a/modules/nf-core/opt/track/main.nf +++ b/modules/nf-core/opt/track/main.nf @@ -2,7 +2,10 @@ process OPT_TRACK { tag "$meta.id" label 'process_high' - container "khersameesh24/opt:v0.0.1" + conda "${moduleDir}/environment.yml" + container "${workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/64/64550ef193f98ea294d70e087a80e37bbf0c5c4a5920f0a22414ed8a11c32caa/data' : + 'community.wave.seqera.io/library/opt:0.0.1--f0b1e63f50e38ab1'}" input: tuple val(meta), path(fwd_oriented_fa) @@ -10,50 +13,31 @@ process OPT_TRACK { output: tuple val(meta), path("${prefix}/probe2targets.tsv"), emit: probes2target - path "versions.yml" , emit: versions + tuple val("${task.process}"), val('opt'), eval("opt --version"), topic: versions, emit: versions_opt when: task.ext.when == null || task.ext.when script: - // Exit if running this module with -profile conda / -profile mamba - if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { - error "OPT_TRACK module does not support Conda. Please use Docker / Singularity / Podman instead." - } def args = task.ext.args ?: '' prefix = task.ext.prefix ?: "${meta.id}" """ opt \\ - -o ${prefix} \\ - -p ${task.cpus} \\ - track \\ - -q ${fwd_oriented_fa} \\ - -a ${ref_annot_gff} \\ - -t ${ref_annot_fa} \\ - ${args} - - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - opt: \$(opt --version) - END_VERSIONS + -o ${prefix} \\ + -p ${task.cpus} \\ + track \\ + -q ${fwd_oriented_fa} \\ + -a ${ref_annot_gff} \\ + -t ${ref_annot_fa} \\ + ${args} """ stub: - // Exit if running this module with -profile conda / -profile mamba - if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { - error "OPT_TRACK module does not support Conda. Please use Docker / Singularity / Podman instead." - } prefix = task.ext.prefix ?: "${meta.id}" - + """ mkdir -p ${prefix} touch "${prefix}/probe2targets.tsv" - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - opt: \$(opt --version) - END_VERSIONS """ } diff --git a/modules/nf-core/opt/track/meta.yml b/modules/nf-core/opt/track/meta.yml index 6024d773..1d4dbd16 100644 --- a/modules/nf-core/opt/track/meta.yml +++ b/modules/nf-core/opt/track/meta.yml @@ -1,4 +1,3 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/meta-schema.json name: "opt_track" description: "track aligns query probe sequences to any target transcriptome" keywords: @@ -10,12 +9,14 @@ keywords: - traget transcriptome tools: - "opt": - description: "opt is a simple program that aligns probe sequences to transcript sequences to detect potential off-target probe activity" + description: "opt is a simple program that aligns probe sequences to transcript + sequences to detect potential off-target probe activity" homepage: "https://github.com/JEFworks-Lab/off-target-probe-tracker" documentation: "https://github.com/JEFworks-Lab/off-target-probe-tracker/blob/main/README.md" tool_dev_url: "https://github.com/JEFworks-Lab/off-target-probe-tracker" - licence: [GPL-3.0 license] - + licence: + - GPL-3.0 license + identifier: "" input: - - meta: type: map @@ -42,7 +43,6 @@ input: description: Reference annotation in fasta format pattern: "*.fa" ontologies: [] - output: probes2target: - - meta: @@ -50,19 +50,33 @@ output: description: | Groovy Map containing sample information e.g. `[ id:'sample1' ]` - - "${meta.id}/probe2targets.tsv": + - ${prefix}/probe2targets.tsv: type: file - description: TSV file containing the gene and transcript information to which each probe aligns - pattern: "*.tsv" - ontologies: [] + description: Generated probe targets + pattern: "*probe2targets.tsv" + ontologies: + - edam: http://edamontology.org/format_3475 + versions_opt: + - - ${task.process}: + type: string + description: The name of the process + - opt: + type: string + description: The name of the tool + - opt --version: + type: eval + description: The expression to obtain the version of the tool +topics: versions: - - "versions.yml": - type: file - description: File containing software versions - pattern: "versions.yml" - ontologies: - - edam: "http://edamontology.org/format_3750" # YAML - + - - ${task.process}: + type: string + description: The name of the process + - opt: + type: string + description: The name of the tool + - opt --version: + type: eval + description: The expression to obtain the version of the tool authors: - "@khersameesh24" maintainers: diff --git a/modules/nf-core/opt/track/opt-track.diff b/modules/nf-core/opt/track/opt-track.diff deleted file mode 100644 index 6a0098ad..00000000 --- a/modules/nf-core/opt/track/opt-track.diff +++ /dev/null @@ -1,44 +0,0 @@ -Changes in component 'nf-core/opt/track' -Changes in 'opt/track/main.nf': ---- modules/nf-core/opt/track/main.nf -+++ modules/nf-core/opt/track/main.nf -@@ -9,8 +9,8 @@ - tuple val(meta2), path(ref_annot_gff), path(ref_annot_fa) - - output: -- tuple val(meta), path("${meta.id}/probe2targets.tsv"), emit: probes2target -- path "versions.yml" , emit: versions -+ tuple val(meta), path("${prefix}/probe2targets.tsv"), emit: probes2target -+ path "versions.yml" , emit: versions - - when: - task.ext.when == null || task.ext.when -@@ -21,7 +21,7 @@ - error "OPT_TRACK module does not support Conda. Please use Docker / Singularity / Podman instead." - } - def args = task.ext.args ?: '' -- def prefix = task.ext.prefix ?: "${meta.id}" -+ prefix = task.ext.prefix ?: "${meta.id}" - - """ - opt \\ -@@ -41,8 +41,12 @@ - """ - - stub: -- def prefix = task.ext.prefix ?: "${meta.id}" -- -+ // Exit if running this module with -profile conda / -profile mamba -+ if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { -+ error "OPT_TRACK module does not support Conda. Please use Docker / Singularity / Podman instead." -+ } -+ prefix = task.ext.prefix ?: "${meta.id}" -+ - """ - mkdir -p ${prefix} - touch "${prefix}/probe2targets.tsv" - -'modules/nf-core/opt/track/meta.yml' is unchanged -'modules/nf-core/opt/track/tests/main.nf.test' is unchanged -'modules/nf-core/opt/track/tests/main.nf.test.snap' is unchanged -************************************************************ diff --git a/modules/nf-core/opt/track/tests/main.nf.test b/modules/nf-core/opt/track/tests/main.nf.test index 4fbf9a19..bed4b28b 100644 --- a/modules/nf-core/opt/track/tests/main.nf.test +++ b/modules/nf-core/opt/track/tests/main.nf.test @@ -7,53 +7,72 @@ nextflow_process { tag "modules" tag "modules_nfcore" tag "opt" + tag "opt/flip" tag "opt/track" - test("testrun panel probe sequences") { - - when { + setup { + run("OPT_FLIP") { + script "../../flip/main.nf" process { """ input[0] = [ - [ id:'test_run' ], file('https://raw.githubusercontent.com/khersameesh24/test-datasets/opt/testdata/fwd_oriented.fa', checkIfExists: true) + [ id:'testrun_opt_track' ], + file(params.modules_testdata_base_path + 'spatial_omics/probe_detection/probes.fa', checkIfExists: true) ] input[1] = [ - [ id:'test_run' ], - file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.gtf', checkIfExists: true), - file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta', checkIfExists: true) + [ id:'test_references' ], + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/minigenome.gtf', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/minigenome.fa', checkIfExists: true) ] """ } } + } + + test("testrun opt/track panel probe sequences") { + + when { + process { + """ + input[0] = OPT_FLIP.out.fwd_oriented_fa + input[1] = [ + [ id:'test_references' ], + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/minigenome.gtf', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/minigenome.fa', checkIfExists: true) + ] + """ + } + } + then { assertAll( { assert process.success }, - { assert snapshot(process.out).match() } + { assert snapshot(sanitizeOutput(process.out)).match() } ) } } - test("testrun panel probe sequences -stub") { + test("testrun opt/track panel probe sequences -stub") { + options "-stub" + when { process { """ - input[0] = [ - [ id:'test_run' ], file('https://raw.githubusercontent.com/khersameesh24/test-datasets/opt/testdata/fwd_oriented.fa', checkIfExists: true) - ] - + input[0] = OPT_FLIP.out.fwd_oriented_fa input[1] = [ - [ id:'test_run' ], - file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.gtf', checkIfExists: true), - file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta', checkIfExists: true) + [ id:'test_references' ], + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/minigenome.gtf', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/minigenome.fa', checkIfExists: true) ] """ } } + then { assertAll( { assert process.success }, - { assert snapshot(process.out).match() } + { assert snapshot(sanitizeOutput(process.out)).match() } ) } } diff --git a/modules/nf-core/opt/track/tests/main.nf.test.snap b/modules/nf-core/opt/track/tests/main.nf.test.snap index 3dda7a91..88456d6e 100644 --- a/modules/nf-core/opt/track/tests/main.nf.test.snap +++ b/modules/nf-core/opt/track/tests/main.nf.test.snap @@ -1,68 +1,54 @@ { - "testrun panel probe sequences -stub": { + "testrun opt/track panel probe sequences -stub": { "content": [ { - "0": [ + "probes2target": [ [ { - "id": "test_run" + "id": "testrun_opt_track" }, "probe2targets.tsv:md5,d41d8cd98f00b204e9800998ecf8427e" ] ], - "1": [ - "versions.yml:md5,daf660f286a817fa0eed7703a5f65706" - ], - "probes2target": [ + "versions_opt": [ [ - { - "id": "test_run" - }, - "probe2targets.tsv:md5,d41d8cd98f00b204e9800998ecf8427e" + "OPT_TRACK", + "opt", + "opt v0.0.1" ] - ], - "versions": [ - "versions.yml:md5,daf660f286a817fa0eed7703a5f65706" ] } ], + "timestamp": "2026-06-04T11:37:32.832421175", "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.6" - }, - "timestamp": "2025-09-14T12:20:29.220245783" + "nf-test": "0.9.5", + "nextflow": "26.04.3" + } }, - "testrun panel probe sequences": { + "testrun opt/track panel probe sequences": { "content": [ { - "0": [ + "probes2target": [ [ { - "id": "test_run" + "id": "testrun_opt_track" }, "probe2targets.tsv:md5,e15465df3845d7a6acf64dd3be04391b" ] ], - "1": [ - "versions.yml:md5,daf660f286a817fa0eed7703a5f65706" - ], - "probes2target": [ + "versions_opt": [ [ - { - "id": "test_run" - }, - "probe2targets.tsv:md5,e15465df3845d7a6acf64dd3be04391b" + "OPT_TRACK", + "opt", + "opt v0.0.1" ] - ], - "versions": [ - "versions.yml:md5,daf660f286a817fa0eed7703a5f65706" ] } ], + "timestamp": "2026-06-04T11:37:13.744717475", "meta": { - "nf-test": "0.9.2", - "nextflow": "25.04.6" - }, - "timestamp": "2025-09-14T12:20:19.999359259" + "nf-test": "0.9.5", + "nextflow": "26.04.3" + } } } \ No newline at end of file diff --git a/modules/nf-core/proseg/proseg/environment.yml b/modules/nf-core/proseg/proseg/environment.yml new file mode 100755 index 00000000..15063256 --- /dev/null +++ b/modules/nf-core/proseg/proseg/environment.yml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - bioconda::rust-proseg=3.1.1 diff --git a/modules/nf-core/proseg/proseg/main.nf b/modules/nf-core/proseg/proseg/main.nf new file mode 100755 index 00000000..8b70d93c --- /dev/null +++ b/modules/nf-core/proseg/proseg/main.nf @@ -0,0 +1,85 @@ +process PROSEG { + tag "$meta.id" + label 'process_high' + + conda "${moduleDir}/environment.yml" + container "${workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/5c/5c9a638dd3ab6d2ce5f366a8aaec27dc34dcf2914f01112a22b855c71bf58a7b/data' : + 'community.wave.seqera.io/library/rust-proseg:3.1.1--5dbb81bc6361cb10'}" + + input: + tuple val(meta), path(transcripts) + val mode + tuple val(transcript_metadata_fmt), val(cell_metadata_fmt), val(expected_counts_fmt) + + output: + tuple val(meta), path("${prefix}.zarr" , arity: '1'), emit: zarr + tuple val(meta), path("${prefix}_transcript-metadata.{csv,csv.gz,parquet}", arity: '1'), emit: transcript_metadata + tuple val(meta), path("${prefix}_cell-polygons.geojson.gz" , arity: '1'), emit: cell_polygons + tuple val(meta), path("${prefix}_cell-metadata.{csv,csv.gz,parquet}" , arity: '1'), emit: cell_metadata + tuple val(meta), path("${prefix}_cell-polygons-layers.geojson.gz" , arity: '1'), emit: cell_polygons_layers + tuple val(meta), path("${prefix}_expected-counts.{csv,csv.gz,parquet}" , arity: '1'), emit: expected_counts + tuple val(meta), path("${prefix}_cell-polygons-union.geojson.gz" , arity: '1'), emit: union_cell_polygons + tuple val(meta), path("${prefix}_maxpost-counts.{csv,csv.gz,parquet}" ), emit: maxpost_counts , optional: true + tuple val(meta), path("${prefix}_output-rates.{csv,csv.gz,parquet}" ), emit: output_rates , optional: true + tuple val(meta), path("${prefix}_cell-hulls.{csv,csv.gz,parquet}" ), emit: cell_hulls , optional: true + tuple val(meta), path("${prefix}_gene-metadata.{csv,csv.gz,parquet}" ), emit: gene_metadata , optional: true + tuple val(meta), path("${prefix}_metagene-rates.{csv,csv.gz,parquet}" ), emit: metagene_rates , optional: true + tuple val(meta), path("${prefix}_metagene-loadings.{csv,csv.gz,parquet}" ), emit: metagene_loadings , optional: true + tuple val(meta), path("${prefix}_cell-voxels.{csv,csv.gz,parquet}" ), emit: cell_voxels , optional: true + tuple val("${task.process}"), val('proseg'), eval("proseg --version | sed 's/proseg //'"), topic: versions, emit: versions_proseg + + when: + task.ext.when == null || task.ext.when + + script: + def preset = mode ? "--${mode}" : '' + def args = task.ext.args ?: '' + prefix = task.ext.prefix ?: "${meta.id}" + + def trans_meta_fmt = transcript_metadata_fmt ?: 'csv.gz' + def cell_meta_fmt = cell_metadata_fmt ?: 'csv.gz' + def exp_counts_fmt = expected_counts_fmt ?: 'csv.gz' + + def output_formats = ['csv', 'csv-gz', 'parquet'] + if (!trans_meta_fmt in output_formats) { + error "Unsupported transcript metadata output format: ${trans_meta_fmt}. Supported formats are: ${output_formats.join(', ')}" + } + if (!cell_meta_fmt in output_formats) { + error "Unsupported cell metadata output format: ${cell_meta_fmt}. Supported formats are: ${output_formats.join(', ')}" + } + if (!exp_counts_fmt in output_formats) { + error "Unsupported expected counts output format: ${exp_counts_fmt}. Supported formats are: ${output_formats.join(', ')}" + } + + """ + proseg \\ + ${preset} \\ + ${args} \\ + --output-spatialdata ${prefix}.zarr \\ + --output-transcript-metadata ${prefix}_transcript-metadata.${trans_meta_fmt} \\ + --output-cell-polygons ${prefix}_cell-polygons.geojson.gz \\ + --output-cell-metadata ${prefix}_cell-metadata.${cell_meta_fmt} \\ + --output-expected-counts ${prefix}_expected-counts.${exp_counts_fmt} \\ + --output-cell-polygon-layers ${prefix}_cell-polygons-layers.geojson.gz \\ + --output-union-cell-polygons ${prefix}_cell-polygons-union.geojson.gz \\ + --nthreads ${task.cpus} \\ + ${transcripts} + """ + + stub: + prefix = task.ext.prefix ?: "${meta.id}" + def trans_meta_fmt = transcript_metadata_fmt ?: 'csv.gz' + def cell_meta_fmt = cell_metadata_fmt ?: 'csv.gz' + def exp_counts_fmt = expected_counts_fmt ?: 'csv.gz' + + """ + touch ${prefix}.zarr + echo "" | gzip > ${prefix}_cell-metadata.${cell_meta_fmt} + echo "" | gzip > ${prefix}_cell-polygons.geojson.gz + echo "" | gzip > ${prefix}_cell-polygons-layers.geojson.gz + echo "" | gzip > ${prefix}_expected-counts.${exp_counts_fmt} + echo "" | gzip > ${prefix}_transcript-metadata.${trans_meta_fmt} + echo "" | gzip > ${prefix}_cell-polygons-union.geojson.gz + """ +} diff --git a/modules/nf-core/proseg/proseg/meta.yml b/modules/nf-core/proseg/proseg/meta.yml new file mode 100755 index 00000000..fa67eb1c --- /dev/null +++ b/modules/nf-core/proseg/proseg/meta.yml @@ -0,0 +1,231 @@ +name: "proseg" +description: "Proseg (probabilistic segmentation) is a cell segmentation method for + in situ spatial transcriptomics." +keywords: + - segmentation + - spatial + - transcriptomics +tools: + - "proseg": + description: "Proseg (probabilistic segmentation) is a cell segmentation method + for in situ spatial transcriptomics." + homepage: "https://github.com/dcjones/proseg" + documentation: "https://github.com/dcjones/proseg" + tool_dev_url: "https://github.com/dcjones/proseg" + doi: "10.1038/s41592-025-02697-0" + licence: + - "GPLv3" + identifier: "" +input: + - - meta: + type: map + description: Groovy Map containing sample information + - transcripts: + type: file + description: Transcript ids, genes, revised positions, assignment + probability, etc. + pattern: "*transcript-metadata.{csv.gz,csv,parquet}" + ontologies: + - edam: http://edamontology.org/format_3752 + - mode: + type: string + description: Proseg preset mode + enum: + - "xenium" + - "merfish" + - "cosmx" + - - transcript_metadata_fmt: + type: string + description: Format of the transcript metadata file + enum: + - "csv" + - "csv.gz" + - "parquet" + - cell_metadata_fmt: + type: file + description: Format of the cell metadata file + enum: + - "csv" + - "csv.gz" + - "parquet" + ontologies: [] + - expected_counts_fmt: + type: file + description: Format of the expected counts file + enum: + - "csv" + - "csv.gz" + - "parquet" + ontologies: [] +output: + zarr: + - - meta: + type: map + description: Groovy Map containing sample information + - ${prefix}.zarr: + type: file + description: "Path to the output spatialdata Zarr file." + pattern: "*.zarr" + ontologies: + - edam: http://edamontology.org/format_3470 + transcript_metadata: + - - meta: + type: map + description: Groovy Map containing sample information + - ${prefix}_transcript-metadata.{csv,csv.gz,parquet}: + type: file + description: Transcript ids, genes, revised positions, assignment + probability, etc. + pattern: "*transcript-metadata.{csv.gz,csv,parquet}" + ontologies: + - edam: http://edamontology.org/format_3752 + cell_polygons: + - - meta: + type: map + description: Groovy Map containing sample information + - ${prefix}_cell-polygons.geojson.gz: + type: file + description: "Path to the cell polygons file." + pattern: "*cell-polygons.geojson.gz" + ontologies: + - edam: http://edamontology.org/format_3464 + - edam: http://edamontology.org/format_3989 + cell_metadata: + - - meta: + type: map + description: Groovy Map containing sample information + - ${prefix}_cell-metadata.{csv,csv.gz,parquet}: + type: file + description: "Path to the cell metadata file." + pattern: "*cell-metadata.{csv.gz,csv,parquet}" + ontologies: + - edam: http://edamontology.org/format_3752 + cell_polygons_layers: + - - meta: + type: map + description: Groovy Map containing sample information + - ${prefix}_cell-polygons-layers.geojson.gz: + type: file + description: "Path to the cell polygons layers file." + pattern: "*cell-polygons-layers.geojson.gz" + ontologies: + - edam: http://edamontology.org/format_3464 + - edam: http://edamontology.org/format_3989 + expected_counts: + - - meta: + type: map + description: Groovy Map containing sample information + - ${prefix}_expected-counts.{csv,csv.gz,parquet}: + type: file + description: "Path to the expected counts file." + pattern: "*expected-counts.{csv.gz,csv,parquet}" + ontologies: + - edam: http://edamontology.org/format_3752 + union_cell_polygons: + - - meta: + type: map + description: Groovy Map containing sample information + - ${prefix}_cell-polygons-union.geojson.gz: + type: file + description: "Path to the union cell polygons file." + pattern: "*cell-polygons-union.geojson.gz" + ontologies: + - edam: http://edamontology.org/format_3464 + - edam: http://edamontology.org/format_3989 + maxpost_counts: + - - meta: + type: map + description: Groovy Map containing sample information + - ${prefix}_maxpost-counts.{csv,csv.gz,parquet}: + type: file + description: "Path to the max posterior counts file." + pattern: "*maxpost-counts.{csv.gz,csv,parquet}" + ontologies: + - edam: http://edamontology.org/format_3752 + output_rates: + - - meta: + type: map + description: Groovy Map containing sample information + - ${prefix}_output-rates.{csv,csv.gz,parquet}: + type: file + description: "Path to the output rates file." + pattern: "*output-rates.{csv.gz,csv,parquet}" + ontologies: + - edam: http://edamontology.org/format_3752 + cell_hulls: + - - meta: + type: map + description: Groovy Map containing sample information + - ${prefix}_cell-hulls.{csv,csv.gz,parquet}: + type: file + description: "Path to the cell hulls file." + pattern: "*cell-hulls.{csv.gz,csv,parquet}" + ontologies: + - edam: http://edamontology.org/format_3752 + gene_metadata: + - - meta: + type: map + description: Groovy Map containing sample information + - ${prefix}_gene-metadata.{csv,csv.gz,parquet}: + type: file + description: "Path to the gene metadata file." + pattern: "*gene-metadata.{csv.gz,csv,parquet}" + ontologies: + - edam: http://edamontology.org/format_3752 + metagene_rates: + - - meta: + type: map + description: Groovy Map containing sample information + - ${prefix}_metagene-rates.{csv,csv.gz,parquet}: + type: file + description: "Path to the metagene rates file." + pattern: "*metagene-rates.{csv.gz,csv,parquet}" + ontologies: + - edam: http://edamontology.org/format_3752 + metagene_loadings: + - - meta: + type: map + description: Groovy Map containing sample information + - ${prefix}_metagene-loadings.{csv,csv.gz,parquet}: + type: file + description: "Path to the metagene loadings file." + pattern: "*metagene-loadings.{csv.gz,csv,parquet}" + ontologies: + - edam: http://edamontology.org/format_3752 + cell_voxels: + - - meta: + type: map + description: Groovy Map containing sample information + - ${prefix}_cell-voxels.{csv,csv.gz,parquet}: + type: file + description: "Path to the cell voxels file." + pattern: "*cell-voxels.{csv.gz,csv,parquet}" + ontologies: + - edam: http://edamontology.org/format_3752 + versions_proseg: + - - ${task.process}: + type: string + description: The name of the process + - proseg: + type: string + description: The name of the tool + - proseg --version | sed 's/proseg //': + type: eval + description: The expression to obtain the version of the tool +topics: + versions: + - - ${task.process}: + type: string + description: The name of the process + - proseg: + type: string + description: The name of the tool + - proseg --version | sed 's/proseg //': + type: eval + description: The expression to obtain the version of the tool +authors: + - "@derrik-gratz" + - "@khersameesh24" +maintainers: + - "@derrik-gratz" + - "@khersameesh24" diff --git a/modules/local/proseg/preset/tests/main.nf.test b/modules/nf-core/proseg/proseg/tests/main.nf.test old mode 100644 new mode 100755 similarity index 57% rename from modules/local/proseg/preset/tests/main.nf.test rename to modules/nf-core/proseg/proseg/tests/main.nf.test index baa48c10..50d0a046 --- a/modules/local/proseg/preset/tests/main.nf.test +++ b/modules/nf-core/proseg/proseg/tests/main.nf.test @@ -20,6 +20,8 @@ nextflow_process { [id: "test_run_proseg"], file(params.modules_testdata_base_path + "spatial_omics/xenium/homo_sapiens/spatial_gene_expression.csv", checkIfExists: true) ] + input[1] = 'xenium' + input[2] = ['csv.gz', 'csv.gz', 'csv.gz'] """ } } @@ -27,11 +29,19 @@ nextflow_process { then { assertAll( { assert process.success }, - { assert file(process.out.seg_outs[0][1]).exists() }, - { assert file(process.out.seg_outs[0][1]).name == "cell-polygons.geojson.gz" }, - { assert file(process.out.seg_outs[0][2]).exists() }, - { assert file(process.out.seg_outs[0][2]).name == "transcript-metadata.csv.gz" }, - { assert snapshot(process.out.versions_proseg).match("versions") } + { assert snapshot(sanitizeOutput( + process.out, + unstableKeys: [ + "zarr", + "cell_polygons", + "transcript_metadata", + "cell_metadata", + "cell_polygons_layers", + "expected_counts", + "union_cell_polygons" + ] + )).match() + } ) } } @@ -47,6 +57,8 @@ nextflow_process { [id: "test_run_proseg"], file(params.modules_testdata_base_path + "spatial_omics/xenium/homo_sapiens/spatial_gene_expression.csv", checkIfExists: true) ] + input[1] = 'xenium' + input[2] = ['csv.gz', 'csv.gz', 'csv.gz'] """ } } @@ -54,7 +66,7 @@ nextflow_process { then { assertAll( { assert process.success }, - { assert snapshot(process.out.versions_proseg).match("versions_stub") } + { assert snapshot(sanitizeOutput(process.out)).match() } ) } } diff --git a/modules/nf-core/proseg/proseg/tests/main.nf.test.snap b/modules/nf-core/proseg/proseg/tests/main.nf.test.snap new file mode 100644 index 00000000..c67c52d0 --- /dev/null +++ b/modules/nf-core/proseg/proseg/tests/main.nf.test.snap @@ -0,0 +1,192 @@ +{ + "proseg - transcripts.csv": { + "content": [ + { + "cell_hulls": [ + + ], + "cell_metadata": [ + [ + { + "id": "test_run_proseg" + }, + "test_run_proseg_cell-metadata.csv.gz" + ] + ], + "cell_polygons": [ + [ + { + "id": "test_run_proseg" + }, + "test_run_proseg_cell-polygons.geojson.gz" + ] + ], + "cell_polygons_layers": [ + [ + { + "id": "test_run_proseg" + }, + "test_run_proseg_cell-polygons-layers.geojson.gz" + ] + ], + "cell_voxels": [ + + ], + "expected_counts": [ + [ + { + "id": "test_run_proseg" + }, + "test_run_proseg_expected-counts.csv.gz" + ] + ], + "gene_metadata": [ + + ], + "maxpost_counts": [ + + ], + "metagene_loadings": [ + + ], + "metagene_rates": [ + + ], + "output_rates": [ + + ], + "transcript_metadata": [ + [ + { + "id": "test_run_proseg" + }, + "test_run_proseg_transcript-metadata.csv.gz" + ] + ], + "union_cell_polygons": [ + [ + { + "id": "test_run_proseg" + }, + "test_run_proseg_cell-polygons-union.geojson.gz" + ] + ], + "versions_proseg": [ + [ + "PROSEG", + "proseg", + "3.1.1" + ] + ], + "zarr": [ + [ + { + "id": "test_run_proseg" + }, + "test_run_proseg.zarr" + ] + ] + } + ], + "timestamp": "2026-06-01T18:57:10.896896692", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.3" + } + }, + "proseg stub": { + "content": [ + { + "cell_hulls": [ + + ], + "cell_metadata": [ + [ + { + "id": "test_run_proseg" + }, + "test_run_proseg_cell-metadata.csv.gz:md5,68b329da9893e34099c7d8ad5cb9c940" + ] + ], + "cell_polygons": [ + [ + { + "id": "test_run_proseg" + }, + "test_run_proseg_cell-polygons.geojson.gz:md5,68b329da9893e34099c7d8ad5cb9c940" + ] + ], + "cell_polygons_layers": [ + [ + { + "id": "test_run_proseg" + }, + "test_run_proseg_cell-polygons-layers.geojson.gz:md5,68b329da9893e34099c7d8ad5cb9c940" + ] + ], + "cell_voxels": [ + + ], + "expected_counts": [ + [ + { + "id": "test_run_proseg" + }, + "test_run_proseg_expected-counts.csv.gz:md5,68b329da9893e34099c7d8ad5cb9c940" + ] + ], + "gene_metadata": [ + + ], + "maxpost_counts": [ + + ], + "metagene_loadings": [ + + ], + "metagene_rates": [ + + ], + "output_rates": [ + + ], + "transcript_metadata": [ + [ + { + "id": "test_run_proseg" + }, + "test_run_proseg_transcript-metadata.csv.gz:md5,68b329da9893e34099c7d8ad5cb9c940" + ] + ], + "union_cell_polygons": [ + [ + { + "id": "test_run_proseg" + }, + "test_run_proseg_cell-polygons-union.geojson.gz:md5,68b329da9893e34099c7d8ad5cb9c940" + ] + ], + "versions_proseg": [ + [ + "PROSEG", + "proseg", + "3.1.1" + ] + ], + "zarr": [ + [ + { + "id": "test_run_proseg" + }, + "test_run_proseg.zarr:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ] + } + ], + "timestamp": "2026-06-01T18:57:17.727702028", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.3" + } + } +} \ No newline at end of file diff --git a/modules/nf-core/proseg/proseg2baysor/environment.yml b/modules/nf-core/proseg/proseg2baysor/environment.yml new file mode 100755 index 00000000..15063256 --- /dev/null +++ b/modules/nf-core/proseg/proseg2baysor/environment.yml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - bioconda::rust-proseg=3.1.1 diff --git a/modules/nf-core/proseg/proseg2baysor/main.nf b/modules/nf-core/proseg/proseg2baysor/main.nf new file mode 100644 index 00000000..a0b8a896 --- /dev/null +++ b/modules/nf-core/proseg/proseg2baysor/main.nf @@ -0,0 +1,40 @@ +process PROSEG2BAYSOR { + tag "$meta.id" + label 'process_low' + + conda "${moduleDir}/environment.yml" + container "${workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/5c/5c9a638dd3ab6d2ce5f366a8aaec27dc34dcf2914f01112a22b855c71bf58a7b/data' : + 'community.wave.seqera.io/library/rust-proseg:3.1.1--5dbb81bc6361cb10'}" + + input: + tuple val(meta), path(sd_zarr) + + output: + tuple val(meta), path("${prefix}_cell-polygons.geojson") , emit: cell_polygons + tuple val(meta), path("${prefix}_transcript-metadata.csv"), emit: transcript_metadata + tuple val("${task.process}"), val('proseg'), eval("proseg --version | sed 's/proseg //'"), topic: versions, emit: versions_proseg + + when: + task.ext.when == null || task.ext.when + + script: + def args = task.ext.args ?: '' + prefix = task.ext.prefix ?: "${meta.id}" + + """ + proseg-to-baysor \\ + --output-transcript-metadata ${prefix}_transcript-metadata.csv \\ + --output-cell-polygons ${prefix}_cell-polygons.geojson \\ + ${sd_zarr} \\ + ${args} + """ + + stub: + prefix = task.ext.prefix ?: "${meta.id}" + + """ + touch ${prefix}_transcript-metadata.csv + touch ${prefix}_cell-polygons.geojson + """ +} diff --git a/modules/nf-core/proseg/proseg2baysor/meta.yml b/modules/nf-core/proseg/proseg2baysor/meta.yml new file mode 100755 index 00000000..9004f313 --- /dev/null +++ b/modules/nf-core/proseg/proseg2baysor/meta.yml @@ -0,0 +1,79 @@ +name: "proseg2baysor" +description: Convert proseg outputs to baysor format for import to Xenium + explorer +keywords: + - segmentation + - spatial + - transcriptomics +tools: + - "proseg": + description: "Proseg (probabilistic segmentation) is a cell segmentation method + for in situ spatial transcriptomics." + homepage: "https://github.com/dcjones/proseg" + documentation: "https://github.com/dcjones/proseg" + tool_dev_url: "https://github.com/dcjones/proseg" + doi: "10.1038/s41592-025-02697-0" + licence: + - "GPLv3" + identifier: "" +input: + - - meta: + type: map + description: Groovy Map containing sample information + - sd_zarr: + type: file + description: "Path to the spatialdata Zarr file output by proseg." + pattern: "*.zarr" + ontologies: + - edam: http://edamontology.org/format_3470 +output: + cell_polygons: + - - meta: + type: map + description: Groovy Map containing sample information + - ${prefix}_cell-polygons.geojson: + type: file + description: "Path to the cell polygons file." + pattern: "*cell-polygons.geojson.gz" + ontologies: + - edam: http://edamontology.org/format_3464 + - edam: http://edamontology.org/format_3989 + transcript_metadata: + - - meta: + type: map + description: Groovy Map containing sample information + - ${prefix}_transcript-metadata.csv: + type: file + description: Transcript ids, genes, revised positions, assignment + probability, etc. + pattern: "*transcript-metadata.csv.gz" + ontologies: + - edam: http://edamontology.org/format_3752 + - edam: http://edamontology.org/format_3989 + versions_proseg: + - - ${task.process}: + type: string + description: The name of the process + - proseg: + type: string + description: The name of the tool + - proseg --version | sed 's/proseg //': + type: eval + description: The expression to obtain the version of the tool +topics: + versions: + - - ${task.process}: + type: string + description: The name of the process + - proseg: + type: string + description: The name of the tool + - proseg --version | sed 's/proseg //': + type: eval + description: The expression to obtain the version of the tool +authors: + - "@derrik-gratz" + - "@khersameesh24" +maintainers: + - "@derrik-gratz" + - "@khersameesh24" diff --git a/modules/nf-core/proseg/proseg2baysor/tests/main.nf.test b/modules/nf-core/proseg/proseg2baysor/tests/main.nf.test new file mode 100755 index 00000000..a67e3fd3 --- /dev/null +++ b/modules/nf-core/proseg/proseg2baysor/tests/main.nf.test @@ -0,0 +1,77 @@ +nextflow_process { + + name "Test Process PROSEG2BAYSOR" + script "../main.nf" + process "PROSEG2BAYSOR" + + tag "modules" + tag "modules_nfcore" + tag "proseg/proseg2baysor" + tag "proseg/proseg" + tag "proseg" + tag "segmentation" + tag "cell_segmentation" + + + setup { + run("PROSEG") { + script "../../proseg/main.nf" + process { + """ + input[0] = [ + [id: "testrun_proseg"], + file(params.modules_testdata_base_path + "spatial_omics/xenium/homo_sapiens/spatial_gene_expression.csv", checkIfExists: true) + ] + input[1] = 'xenium' + input[2] = ['csv.gz', 'csv.gz', 'csv.gz'] + """ + } + } + } + + test("proseg-to-baysor - transcripts.csv") { + + when { + process { + """ + input[0] = PROSEG.out.zarr + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(sanitizeOutput( + process.out, + unstableKeys: [ + "cell_polygons", + "transcript_metadata", + ] + )).match() + } + ) + } + + } + + test("proseg-to-baysor stub") { + + options "-stub" + + when { + process { + """ + input[0] = PROSEG.out.zarr + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(sanitizeOutput(process.out)).match() } + ) + } + } +} diff --git a/modules/nf-core/proseg/proseg2baysor/tests/main.nf.test.snap b/modules/nf-core/proseg/proseg2baysor/tests/main.nf.test.snap new file mode 100644 index 00000000..ab262964 --- /dev/null +++ b/modules/nf-core/proseg/proseg2baysor/tests/main.nf.test.snap @@ -0,0 +1,70 @@ +{ + "proseg-to-baysor - transcripts.csv": { + "content": [ + { + "cell_polygons": [ + [ + { + "id": "testrun_proseg" + }, + "testrun_proseg_cell-polygons.geojson" + ] + ], + "transcript_metadata": [ + [ + { + "id": "testrun_proseg" + }, + "testrun_proseg_transcript-metadata.csv" + ] + ], + "versions_proseg": [ + [ + "PROSEG2BAYSOR", + "proseg", + "3.1.1" + ] + ] + } + ], + "timestamp": "2026-06-01T18:52:42.329199284", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.3" + } + }, + "proseg-to-baysor stub": { + "content": [ + { + "cell_polygons": [ + [ + { + "id": "testrun_proseg" + }, + "testrun_proseg_cell-polygons.geojson:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "transcript_metadata": [ + [ + { + "id": "testrun_proseg" + }, + "testrun_proseg_transcript-metadata.csv:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "versions_proseg": [ + [ + "PROSEG2BAYSOR", + "proseg", + "3.1.1" + ] + ] + } + ], + "timestamp": "2026-06-01T18:52:49.467682999", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.3" + } + } +} \ No newline at end of file diff --git a/nextflow.config b/nextflow.config index cf75ae06..93b2a939 100644 --- a/nextflow.config +++ b/nextflow.config @@ -18,11 +18,6 @@ params { qupath_polygons = null // Path to qupath segmentation results in GeoJSON format alignment_csv = null // image alignment file format a 3x3 transformation matrix, where the last row is [0,0,1] cellpose_model = null // custom cellpose model to use for running or starting training - stardist_model = '2D_versatile_fluo' // stardist pretrained model for cell segmentation - stardist_nuclei_model = '2D_versatile_fluo' // stardist pretrained model for nuclei segmentation - stardist_prob_thresh = null // stardist probability threshold - stardist_nms_thresh = null // stardist NMS threshold - stardist_n_tiles = "8 8" // tiling for large images (Xenium images are ~20K×25K) segmentation_mask = null // prior segmentation mask probes_fasta = null // Fasta file for the probe sequences used in the xenium experiment reference_annotations = null // Path to the genomic features (.gff) and fasta (.fa) files used as reference annotations @@ -39,21 +34,6 @@ params { cell_segmentation_only = true // to only run cell segmentation while running segmentation methods & XR_IMP-SEG cellpose_downscale = false // pre-downscale morphology image to avoid cellpose OOM on large images - // Xeniumranger specific - xeniumranger_only = false // to generate redefined bundle with just changing the xr specific params - relabel_genes = false // wether to correct gene names with gene_panel.json - expansion_distance = 5 // default nuclear expansion distance in XOA v2.0 & later - dapi_filter = 100 // adjust the minimum peak intensity to use more nuclei - interior_stain = true // interior stain is enabled by default - false to disable - boundary_stain = true // boundary stain is enabled by default - false to disable - - // Segger specific - segmentation_refinement = false // wether to run segmentation refinement step (segger) - segger_accelerator = 'cpu' // either 'cuda' or 'cpu' - segger_knn_method = 'kd_tree' // 'cuda' - ensure your system has CUDA installed and configured properly - segger_num_workers = 4 // number of data-loader workers for segger - segger_model = null // path to a pre-trained segger model checkpoint - // Proseg specific format = 'xenium' // preset value set as `xenium` @@ -62,18 +42,6 @@ params { transcript_seg_methods = ["proseg", "segger", "baysor"] segfree_methods = ["ficture", "baysor"] - // Ficture specific - negative_control_regex = null - features = null - - // Baysor specific - filter_transcripts = false - min_qv = 20 - max_x = 24000.0 - min_x = 0.0 - max_y = 24000.0 - min_y = 0.0 - // Generic tiling parameters (for proseg and other methods) tiling = false // enable tiled segmentation (divide → parallel segmentation → stitch) patch_grid = '3x3' // grid layout for tiling (rows x cols) @@ -82,33 +50,6 @@ params { patch_filter_iqr_multiplier = 3.0 // IQR multiplier for empirical cell size filtering patch_filter_z_threshold = 4.0 // z-score threshold for distribution cell size filtering - // Baysor-specific parameters - baysor_scale = 30 // Baysor --scale for non-tiled runs - baysor_config = null // path to baysor config TOML (optional) - baysor_tiling = true // enable tiled Baysor (divide → per-patch Baysor → stitch) - baysor_tiling_micron = 1200 // tile width in microns for Baysor tiling - baysor_tiling_overlap = 200 // overlap between Baysor patches in microns - baysor_tiling_balanced = true // balance transcripts across tiles (merge sparse tiles) - baysor_tiling_scale = 39 // Baysor --scale for tiled runs (larger to compensate for EM on tiles) - baysor_tiling_min_mols_per_cell = 120 // --min-molecules-per-cell for tiled Baysor - baysor_tiling_min_transcripts_per_cell = 50 // post-stitch cell filtering threshold - - // Baysor prior segmentation - // null — no prior (random EM init) - // 'cells' — Xenium bundle cell_id column (column-based, works with tiling) - // 'cellpose' — run Cellpose cell mask as image prior (non-tiled only) - baysor_prior = null - baysor_prior_confidence = 0.2 // prior-segmentation-confidence [0-1] - - // Segger specific - tile_width = 120 - tile_height = 120 - batch_size_train = 4 // larger batch size can speed up training, but requires more memory - devices = 4 // Use multiple GPUs by increasing the devices parameter to further accelerate training - max_epochs = 200 // increasing #epochs can improve model performance with more learning cycles, but extends training time - batch_size_predict = 1 // larger batch size can speed up training, but requires more memory - cc_analysis = false // to control connected component analysis - // qc specific run_qc = true // whether to run the qc layer of pipeline offtarget_probe_tracking = false // whether to run off-target probe tracking (provide probe_fasta, reference sequences, gene synonyms ) @@ -118,10 +59,10 @@ params { csplit_y_bins = 2 // number of tiles along the y axis // MultiQC options - multiqc_config = null - multiqc_title = null - multiqc_logo = null - max_multiqc_email_size = '25.MB' + multiqc_config = null + multiqc_title = null + multiqc_logo = null + max_multiqc_email_size = '25.MB' multiqc_methods_description = null // pipeline dev and testing option @@ -161,6 +102,14 @@ params { // Load base.config by default for all pipelines includeConfig 'conf/base.config' +// load tool-specific configs +includeConfig 'conf/methods/baysor.config' +includeConfig 'conf/methods/proseg.config' +includeConfig 'conf/methods/segger.config' +includeConfig 'conf/methods/stardist.config' +includeConfig 'conf/methods/ficture.config' +includeConfig 'conf/methods/xeniumranger.config' + profiles { debug { dumpHashes = true @@ -381,7 +330,7 @@ manifest { ] ] homePage = 'https://github.com/nf-core/spatialaxe' - description = """A pipeline for spatialomics 10x Xenium In Situ data.""" + description = """A pipeline to process spatialomics data from 10x Xenium In Situ or 10x Atera.""" mainScript = 'main.nf' defaultBranch = 'master' nextflowVersion = '!>=25.04.0' diff --git a/nextflow_schema.json b/nextflow_schema.json index 58a47276..099a5433 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -309,6 +309,11 @@ "enum": ["cells", "cellpose"], "description": "Prior segmentation type for Baysor. 'cells' uses Xenium bundle cell_id column; 'cellpose' uses Cellpose mask as image prior." }, + "min_transcripts_per_cell": { + "type": "integer", + "default": 10, + "description": "Minimum number of transcripts per cell for filtering." + }, "baysor_prior_confidence": { "type": "number", "default": 0.2, @@ -321,18 +326,22 @@ }, "min_x": { "type": "number", + "default": 0.0, "description": "only keep transcripts whose x-coordinate is greater than specified limit, if no limit is specified, the default minimum value will be 0.0" }, "max_x": { "type": "number", + "default": 24000.0, "description": "only keep transcripts whose x-coordinate is less than specified limit, if no limit is specified, the default value will retain all transcripts since Xenium slide is <24000 microns in x and y (default: 24000.0)" }, "min_y": { "type": "number", + "default": 0.0, "description": "only keep transcripts whose y-coordinate is greater than specified limit, if no limit is specified, the default minimum value will be 0.0" }, "max_y": { "type": "number", + "default": 24000.0, "description": "only keep transcripts whose y-coordinate is less than specified limit, if no limit is specified, the default value will retain all transcripts since Xenium slide is <24000 microns in x and y (default: 24000.0)" }, "tiling": { diff --git a/ro-crate-metadata.json b/ro-crate-metadata.json index d5575c0b..05fbf776 100644 --- a/ro-crate-metadata.json +++ b/ro-crate-metadata.json @@ -23,7 +23,7 @@ "@type": "Dataset", "creativeWorkStatus": "InProgress", "datePublished": "2026-06-17T15:15:09+00:00", - "description": "

\n \n \n \"nf-core/spatialaxe\"\n \n

\n\n[![Open in GitHub Codespaces](https://img.shields.io/badge/Open_In_GitHub_Codespaces-black?labelColor=grey&logo=github)](https://github.com/codespaces/new/nf-core/spatialaxe)\n[![GitHub Actions CI Status](https://github.com/nf-core/spatialaxe/actions/workflows/nf-test.yml/badge.svg)](https://github.com/nf-core/spatialaxe/actions/workflows/nf-test.yml)\n[![GitHub Actions Linting Status](https://github.com/nf-core/spatialaxe/actions/workflows/linting.yml/badge.svg)](https://github.com/nf-core/spatialaxe/actions/workflows/linting.yml)[![AWS CI](https://img.shields.io/badge/CI%20tests-full%20size-FF9900?labelColor=000000&logo=Amazon%20AWS)](https://nf-co.re/spatialaxe/results)[![Cite with Zenodo](http://img.shields.io/badge/DOI-10.5281/zenodo.XXXXXXX-1073c8?labelColor=000000)](https://doi.org/10.5281/zenodo.XXXXXXX)\n[![nf-test](https://img.shields.io/badge/unit_tests-nf--test-337ab7.svg)](https://www.nf-test.com)\n\n[![Nextflow](https://img.shields.io/badge/version-%E2%89%A525.04.0-green?style=flat&logo=nextflow&logoColor=white&color=%230DC09D&link=https%3A%2F%2Fnextflow.io)](https://www.nextflow.io/)\n[![nf-core template version](https://img.shields.io/badge/nf--core_template-3.4.1-green?style=flat&logo=nfcore&logoColor=white&color=%2324B064&link=https%3A%2F%2Fnf-co.re)](https://github.com/nf-core/tools/releases/tag/3.4.1)\n[![run with docker](https://img.shields.io/badge/run%20with-docker-0db7ed?labelColor=000000&logo=docker)](https://www.docker.com/)\n[![run with singularity](https://img.shields.io/badge/run%20with-singularity-1d355c.svg?labelColor=000000)](https://sylabs.io/docs/)\n[![Launch on Seqera Platform](https://img.shields.io/badge/Launch%20%F0%9F%9A%80-Seqera%20Platform-%234256e7)](https://cloud.seqera.io/launch?pipeline=https://github.com/nf-core/spatialaxe)\n\n[![Get help on Slack](http://img.shields.io/badge/slack-nf--core%20%23spatialaxe-4A154B?labelColor=000000&logo=slack)](https://nfcore.slack.com/channels/spatialaxe)[![Follow on Bluesky](https://img.shields.io/badge/bluesky-%40nf__core-1185fe?labelColor=000000&logo=bluesky)](https://bsky.app/profile/nf-co.re)[![Follow on Mastodon](https://img.shields.io/badge/mastodon-nf__core-6364ff?labelColor=FFFFFF&logo=mastodon)](https://mstdn.science/@nf_core)[![Watch on YouTube](http://img.shields.io/badge/youtube-nf--core-FF0000?labelColor=000000&logo=youtube)](https://www.youtube.com/c/nf-core)\n\n## Introduction\n\n**nf-core/spatialaxe** is a bioinformatics best-practice processing and quality control pipeline for Xenium (and soon Atera) data. The current plan for the pipeline implementation is shown in the metromap below. **The pipeline is under active developement and changes might occure frequently**.\n\n![nf-core/spatialaxe-metromap](docs/images/spatialaxe-metromap.png)\n\n> [!NOTE]\n> We are currently extending the pipeline for the [10x Atera system](https://www.10xgenomics.com/platforms/atera).\n\n## Tools supported\n\nThe pipeline supports the following tools:\n\n- Segmenation methods:\n - [Baysor](https://doi.org/10.1038/s41587-021-01044-w)\n - [Cellpose](https://doi.org/10.1038/s41592-020-01018-x)\n - [Xenium ranger (XR)](https://www.10xgenomics.com/support/software/xenium-ranger/latest)\n - [StarDist](https://doi.org/10.48550/arXiv.2203.02284)\n- Segmentation free methods:\n - [Ficture](https://doi.org/10.1038/s41592-024-02415-2)\n - [Baysor](https://doi.org/10.1038/s41587-021-01044-w)\n- Transcript assignment methods:\n - [Segger](https://doi.org/10.1101/2025.03.14.643160)\n - [Proseg](https://doi.org/10.1038/s41592-025-02697-0)\n- Utility methods:\n - [SpatialData](https://doi.org/10.1038/s41592-024-02212-x)\n - [Baysor](https://doi.org/10.1038/s41587-021-01044-w)\n- QC methods:\n - [MultiQC Xenium Extra Plugin](https://github.com/MultiQC/xenium-extra)\n - [OPT](https://github.com/JEFworks-Lab/off-target-probe-tracker)\n\n## Usage\n\nOn release, automated continuous integration tests run the pipeline on a full-sized dataset on the AWS cloud infrastructure. This ensures that the pipeline runs on AWS, has sensible resource allocation defaults set to run on real-world datasets, and permits the persistent storage of results to benchmark between pipeline releases and other analysis sources. The results obtained from the full-sized test can be viewed on the [nf-core website](https://nf-co.re/spatialaxe/results).\n\n> [!NOTE]\n> The pipeline does not support conda currently. We are working on it.\n\n## Quick Start\n\n`samplesheet.csv`:\n\n```csv\nsample,bundle,image\ntest_sample,/path/to/xenium-bundle,/path/to/morphology.ome.tif\n```\n\nNow, you can run the pipeline using:\n\n### Run image-based segmentation mode
\n\n`CELLPOSE -> BAYSOR -> XR-IMPORT_SEGMENTATION -> SPATIALDATA -> QC`\n\n```bash\nnextflow run nf-core/spatialaxe \\\n -profile \\\n --input samplesheet.csv \\\n --outdir \\\n --mode \n```\n\n### Run coordinate-based segmentation mode
\n\n`PROSEG -> PROSEG2BAYSOR -> XR-IMPORT_SEGMENTATION -> SPATIALDATA -> QC`\n\n```bash\nnextflow run nf-core/spatialaxe \\\n -profile \\\n --input samplesheet.csv \\\n --outdir \\\n --mode coordinate\n```\n\n### Run segfree mode
\n\n`BAYSOR_SEGFREE`\n\n```bash\nnextflow run nf-core/spatialaxe \\\n -profile \\\n --input samplesheet.csv \\\n --outdir \\\n --mode segfree\n```\n\n### Run preview mode
\n\n`BAYSOR_PREVIEW`\n\n```bash\nnextflow run nf-core/spatialaxe \\\n -profile \\\n --input samplesheet.csv \\\n --outdir \\\n --mode preview\n```\n\n### Run just the quality control
\n\n```bash\nnextflow run nf-core/spatialaxe \\\n -profile \\\n --input samplesheet.csv \\\n --outdir \\\n --mode qc\n```\n\n### Additional information\n\n> [!WARNING]\n> Please provide pipeline parameters via the CLI or Nextflow `-params-file` option. Custom config files including those provided by the `-c` Nextflow option can be used to provide any configuration _**except for parameters**_; see [docs](https://nf-co.re/docs/usage/getting_started/configuration#custom-configuration-files).\n\nFor more details and further functionality, please refer to the [usage documentation](https://nf-co.re/spatialaxe/usage) and the [parameter documentation](https://nf-co.re/spatialaxe/parameters).\n\n## Pipeline output\n\nTo see the results of an example test run with a full size dataset refer to the [results](https://nf-co.re/spatialaxe/results) tab on the nf-core website pipeline page.\nFor more details about the output files and reports, please refer to the\n[output documentation](https://nf-co.re/spatialaxe/output).\n\n## Runtime and resource estimations\n\n| Tool | Compute | Runtime (min / med / max) | Peak RSS (min / med / max) |\n| ------------------------- | ------- | ------------------------- | -------------------------- |\n| Cellpose | GPU | 1m / 4m / 1.4h | 10 GB / 26 GB / 554 GB |\n| Cellpose | CPU | 1.3h / 2.3h / 6.5h | 161 GB / 426 GB / 1115 GB |\n| StarDist | GPU | 1m / 4m / 7m | 5 GB / 12 GB / 18 GB |\n| StarDist | CPU | 5m / 6m / 7m | 18 GB / 18 GB / 18 GB |\n| Segger (create_dataset) | GPU | 2m / 9m / 31m | 1.7 GB / 14 GB / 50 GB |\n| Segger (create_dataset) | CPU | 13m / 21m / 46m | 13 GB / 19 GB / 49 GB |\n| Segger (train) | GPU | 10m / 43m / 2.9h | 30 GB / 33 GB / 60 GB |\n| Segger (predict) | GPU | 2m / 16m / 59m | 10 GB / 25 GB / 87 GB |\n| Baysor (whole-image) | CPU | 2m / 30m / 17h | 6 GB / 10 GB / 650 GB |\n| Baysor (tiled) | CPU | 1m / 18m / 13h | 0.2 GB / 34 GB / 530 GB |\n| Proseg | CPU | 1m / 18m / 6.8h | 279 MB / 3.8 GB / 136 GB |\n| XeniumRanger (resegment) | CPU | 18m / 39m / 3.7h | 28 GB / 54 GB / 60 GB |\n| XeniumRanger (import_seg) | CPU | 2m / 7m / 2.7h | 2.6 GB / 11 GB / 51 GB |\n| Ficture (preprocess) | CPU | 3m / 4m / 13m | 331 MB / 357 MB / 21 GB |\n\n- Cellpose GPU vs CPU: 35x faster on GPU (4m median vs 2.3h), 16x less memory (26 GB vs 426 GB)\n- Segger: Only tool that truly requires GPU for all 3 steps (create_dataset, train, predict)\n- StarDist: Very fast on CPU, GPU is not necessary to run its default model\n\n## Credits\n\nnf-core/spatialaxe is mainly developed by [Sameesh Kher](https://github.com/khersameesh24), [Dongze He](https://github.com/dongzehe), and [Florian Heyl](https://github.com/heylf).\n\nWe thank the following people for their extensive assistance in the development of this pipeline:\n\n- Tobias Krause\n- Kre\u0161imir Be\u0161tak (kbestak)\n- Matthias H\u00f6rtenhuber (mashehu)\n- Maxime Garcia (maxulysse)\n- K\u00fcbra Narc\u0131 (kubranarci)\n\n## Contributions and Support\n\nIf you would like to contribute to this pipeline, please see the [contributing guidelines](.github/CONTRIBUTING.md).\n\nFor further information or help, don't hesitate to get in touch on the [Slack `#spatialaxe` channel](https://nfcore.slack.com/channels/spatialaxe) (you can join with [this invite](https://nf-co.re/join/slack)).\n\n## Citations\n\n\n\nAn extensive list of references for the tools used by the pipeline can be found in the [`CITATIONS.md`](CITATIONS.md) file.\n\nYou can cite the `nf-core` publication as follows:\n\n> **The nf-core framework for community-curated bioinformatics pipelines.**\n>\n> Philip Ewels, Alexander Peltzer, Sven Fillinger, Harshil Patel, Johannes Alneberg, Andreas Wilm, Maxime Ulysse Garcia, Paolo Di Tommaso & Sven Nahnsen.\n>\n> _Nat Biotechnol._ 2020 Feb 13. doi: [10.1038/s41587-020-0439-x](https://dx.doi.org/10.1038/s41587-020-0439-x).\n", + "description": "

\n \n \n \"nf-core/spatialaxe\"\n \n

\n\n[![Open in GitHub Codespaces](https://img.shields.io/badge/Open_In_GitHub_Codespaces-black?labelColor=grey&logo=github)](https://github.com/codespaces/new/nf-core/spatialaxe)\n[![GitHub Actions CI Status](https://github.com/nf-core/spatialaxe/actions/workflows/nf-test.yml/badge.svg)](https://github.com/nf-core/spatialaxe/actions/workflows/nf-test.yml)\n[![GitHub Actions Linting Status](https://github.com/nf-core/spatialaxe/actions/workflows/linting.yml/badge.svg)](https://github.com/nf-core/spatialaxe/actions/workflows/linting.yml)[![AWS CI](https://img.shields.io/badge/CI%20tests-full%20size-FF9900?labelColor=000000&logo=Amazon%20AWS)](https://nf-co.re/spatialaxe/results)[![Cite with Zenodo](http://img.shields.io/badge/DOI-10.5281/zenodo.20733817-1073c8?labelColor=000000)](https://doi.org/10.5281/zenodo.20733817)\n[![nf-test](https://img.shields.io/badge/unit_tests-nf--test-337ab7.svg)](https://www.nf-test.com)\n\n[![Nextflow](https://img.shields.io/badge/version-%E2%89%A525.04.0-green?style=flat&logo=nextflow&logoColor=white&color=%230DC09D&link=https%3A%2F%2Fnextflow.io)](https://www.nextflow.io/)\n[![nf-core template version](https://img.shields.io/badge/nf--core_template-3.4.1-green?style=flat&logo=nfcore&logoColor=white&color=%2324B064&link=https%3A%2F%2Fnf-co.re)](https://github.com/nf-core/tools/releases/tag/3.4.1)\n[![run with docker](https://img.shields.io/badge/run%20with-docker-0db7ed?labelColor=000000&logo=docker)](https://www.docker.com/)\n[![run with singularity](https://img.shields.io/badge/run%20with-singularity-1d355c.svg?labelColor=000000)](https://sylabs.io/docs/)\n[![Launch on Seqera Platform](https://img.shields.io/badge/Launch%20%F0%9F%9A%80-Seqera%20Platform-%234256e7)](https://cloud.seqera.io/launch?pipeline=https://github.com/nf-core/spatialaxe)\n\n[![Get help on Slack](http://img.shields.io/badge/slack-nf--core%20%23spatialaxe-4A154B?labelColor=000000&logo=slack)](https://nfcore.slack.com/channels/spatialaxe)[![Follow on Bluesky](https://img.shields.io/badge/bluesky-%40nf__core-1185fe?labelColor=000000&logo=bluesky)](https://bsky.app/profile/nf-co.re)[![Follow on Mastodon](https://img.shields.io/badge/mastodon-nf__core-6364ff?labelColor=FFFFFF&logo=mastodon)](https://mstdn.science/@nf_core)[![Watch on YouTube](http://img.shields.io/badge/youtube-nf--core-FF0000?labelColor=000000&logo=youtube)](https://www.youtube.com/c/nf-core)\n\n## Introduction\n\n**nf-core/spatialaxe** is a bioinformatics best-practice processing and quality control pipeline for Xenium (and soon Atera) data. The current plan for the pipeline implementation is shown in the metromap below. **The pipeline is under active developement and changes might occure frequently**.\n\n![nf-core/spatialaxe-metromap](docs/images/spatialaxe-metromap.png)\n\n> [!NOTE]\n> We are currently extending the pipeline for the [10x Atera system](https://www.10xgenomics.com/platforms/atera).\n\n## Tools supported\n\nThe pipeline supports the following tools:\n\n- Segmenation methods:\n - [Baysor](https://doi.org/10.1038/s41587-021-01044-w)\n - [Cellpose](https://doi.org/10.1038/s41592-020-01018-x)\n - [Xenium ranger (XR)](https://www.10xgenomics.com/support/software/xenium-ranger/latest)\n - [StarDist](https://doi.org/10.48550/arXiv.2203.02284)\n- Segmentation free methods:\n - [Ficture](https://doi.org/10.1038/s41592-024-02415-2)\n - [Baysor](https://doi.org/10.1038/s41587-021-01044-w)\n- Transcript assignment methods:\n - [Segger](https://doi.org/10.1101/2025.03.14.643160)\n - [Proseg](https://doi.org/10.1038/s41592-025-02697-0)\n- Utility methods:\n - [SpatialData](https://doi.org/10.1038/s41592-024-02212-x)\n - [Baysor](https://doi.org/10.1038/s41587-021-01044-w)\n- QC methods:\n - [MultiQC Xenium Extra Plugin](https://github.com/MultiQC/xenium-extra)\n - [OPT](https://github.com/JEFworks-Lab/off-target-probe-tracker)\n\n## Usage\n\nOn release, automated continuous integration tests run the pipeline on a full-sized dataset on the AWS cloud infrastructure. This ensures that the pipeline runs on AWS, has sensible resource allocation defaults set to run on real-world datasets, and permits the persistent storage of results to benchmark between pipeline releases and other analysis sources. The results obtained from the full-sized test can be viewed on the [nf-core website](https://nf-co.re/spatialaxe/results).\n\n> [!NOTE]\n> The pipeline does not support conda currently. We are working on it.\n\n## Quick Start\n\n`samplesheet.csv`:\n\n```csv\nsample,bundle,image\ntest_sample,/path/to/xenium-bundle,/path/to/morphology.ome.tif\n```\n\nNow, you can run the pipeline using:\n\n### Run image-based segmentation mode
\n\n`CELLPOSE -> BAYSOR -> XR-IMPORT_SEGMENTATION -> SPATIALDATA -> QC`\n\n```bash\nnextflow run nf-core/spatialaxe \\\n -profile \\\n --input samplesheet.csv \\\n --outdir \\\n --mode \n```\n\n### Run coordinate-based segmentation mode
\n\n`PROSEG -> PROSEG2BAYSOR -> XR-IMPORT_SEGMENTATION -> SPATIALDATA -> QC`\n\n```bash\nnextflow run nf-core/spatialaxe \\\n -profile \\\n --input samplesheet.csv \\\n --outdir \\\n --mode coordinate\n```\n\n### Run segfree mode
\n\n`BAYSOR_SEGFREE`\n\n```bash\nnextflow run nf-core/spatialaxe \\\n -profile \\\n --input samplesheet.csv \\\n --outdir \\\n --mode segfree\n```\n\n### Run preview mode
\n\n`BAYSOR_PREVIEW`\n\n```bash\nnextflow run nf-core/spatialaxe \\\n -profile \\\n --input samplesheet.csv \\\n --outdir \\\n --mode preview\n```\n\n### Run just the quality control
\n\n```bash\nnextflow run nf-core/spatialaxe \\\n -profile \\\n --input samplesheet.csv \\\n --outdir \\\n --mode qc\n```\n\n### Additional information\n\n> [!WARNING]\n> Please provide pipeline parameters via the CLI or Nextflow `-params-file` option. Custom config files including those provided by the `-c` Nextflow option can be used to provide any configuration _**except for parameters**_; see [docs](https://nf-co.re/docs/usage/getting_started/configuration#custom-configuration-files).\n\nFor more details and further functionality, please refer to the [usage documentation](https://nf-co.re/spatialaxe/usage) and the [parameter documentation](https://nf-co.re/spatialaxe/parameters).\n\n## Pipeline output\n\nTo see the results of an example test run with a full size dataset refer to the [results](https://nf-co.re/spatialaxe/results) tab on the nf-core website pipeline page.\nFor more details about the output files and reports, please refer to the\n[output documentation](https://nf-co.re/spatialaxe/output).\n\n## Runtime and resource estimations\n\n| Tool | Compute | Runtime (min / med / max) | Peak RSS (min / med / max) |\n| ------------------------- | ------- | ------------------------- | -------------------------- |\n| Cellpose | GPU | 1m / 4m / 1.4h | 10 GB / 26 GB / 554 GB |\n| Cellpose | CPU | 1.3h / 2.3h / 6.5h | 161 GB / 426 GB / 1115 GB |\n| StarDist | GPU | 1m / 4m / 7m | 5 GB / 12 GB / 18 GB |\n| StarDist | CPU | 5m / 6m / 7m | 18 GB / 18 GB / 18 GB |\n| Segger (create_dataset) | GPU | 2m / 9m / 31m | 1.7 GB / 14 GB / 50 GB |\n| Segger (create_dataset) | CPU | 13m / 21m / 46m | 13 GB / 19 GB / 49 GB |\n| Segger (train) | GPU | 10m / 43m / 2.9h | 30 GB / 33 GB / 60 GB |\n| Segger (predict) | GPU | 2m / 16m / 59m | 10 GB / 25 GB / 87 GB |\n| Baysor (whole-image) | CPU | 2m / 30m / 17h | 6 GB / 10 GB / 650 GB |\n| Baysor (tiled) | CPU | 1m / 18m / 13h | 0.2 GB / 34 GB / 530 GB |\n| Proseg | CPU | 1m / 18m / 6.8h | 279 MB / 3.8 GB / 136 GB |\n| XeniumRanger (resegment) | CPU | 18m / 39m / 3.7h | 28 GB / 54 GB / 60 GB |\n| XeniumRanger (import_seg) | CPU | 2m / 7m / 2.7h | 2.6 GB / 11 GB / 51 GB |\n| Ficture (preprocess) | CPU | 3m / 4m / 13m | 331 MB / 357 MB / 21 GB |\n\n- Cellpose GPU vs CPU: 35x faster on GPU (4m median vs 2.3h), 16x less memory (26 GB vs 426 GB)\n- Segger: Only tool that truly requires GPU for all 3 steps (create_dataset, train, predict)\n- StarDist: Very fast on CPU, GPU is not necessary to run its default model\n\n## Credits\n\nnf-core/spatialaxe is mainly developed by [Sameesh Kher](https://github.com/khersameesh24), [Dongze He](https://github.com/dongzehe), and [Florian Heyl](https://github.com/heylf).\n\nWe thank the following people for their extensive assistance in the development of this pipeline:\n\n- Tobias Krause\n- Kre\u0161imir Be\u0161tak (kbestak)\n- Matthias H\u00f6rtenhuber (mashehu)\n- Maxime Garcia (maxulysse)\n- K\u00fcbra Narc\u0131 (kubranarci)\n\n## Contributions and Support\n\nIf you would like to contribute to this pipeline, please see the [contributing guidelines](.github/CONTRIBUTING.md).\n\nFor further information or help, don't hesitate to get in touch on the [Slack `#spatialaxe` channel](https://nfcore.slack.com/channels/spatialaxe) (you can join with [this invite](https://nf-co.re/join/slack)).\n\n## Citations\n\n\n\nAn extensive list of references for the tools used by the pipeline can be found in the [`CITATIONS.md`](CITATIONS.md) file.\n\nYou can cite the `nf-core` publication as follows:\n\n> **The nf-core framework for community-curated bioinformatics pipelines.**\n>\n> Philip Ewels, Alexander Peltzer, Sven Fillinger, Harshil Patel, Johannes Alneberg, Andreas Wilm, Maxime Ulysse Garcia, Paolo Di Tommaso & Sven Nahnsen.\n>\n> _Nat Biotechnol._ 2020 Feb 13. doi: [10.1038/s41587-020-0439-x](https://dx.doi.org/10.1038/s41587-020-0439-x).\n", "hasPart": [ { "@id": "main.nf" diff --git a/subworkflows/local/baysor_generate_preview/main.nf b/subworkflows/local/baysor_generate_preview/main.nf index 2494fcbd..5f520b50 100644 --- a/subworkflows/local/baysor_generate_preview/main.nf +++ b/subworkflows/local/baysor_generate_preview/main.nf @@ -2,27 +2,28 @@ // Run baysor create_dataset & preview // -include { BAYSOR_PREVIEW } from '../../../modules/local/baysor/preview/main' -include { BAYSOR_CREATE_DATASET } from '../../../modules/local/baysor/create_dataset/main' +include { BAYSOR_PREVIEW } from '../../../modules/nf-core/baysor/preview/main' + +include { BAYSOR_CREATE_DATASET } from '../../../modules/local/utility/create_dataset/main' include { EXTRACT_PREVIEW_DATA } from '../../../modules/local/utility/extract_preview_data/main' -include { PARQUET_TO_CSV } from '../../../modules/local/utility/parquet_to_csv/main' +include { PARQUET2CSV } from '../../../modules/local/utility/parquet2csv/main' workflow BAYSOR_GENERATE_PREVIEW { + take: ch_transcripts_file // channel: [ val(meta), ["path-to-transcripts.parquet"] ] - ch_config // channel: ["path-to-xenium.toml"] + ch_config // channel: ["path-to-xenium.toml"] main: ch_preview_mqc_html = channel.empty() ch_preview_mqc_png = channel.empty() - // run parquet to csv - PARQUET_TO_CSV(ch_transcripts_file, ".csv") + PARQUET2CSV(ch_transcripts_file) // generate randomised sample data - BAYSOR_CREATE_DATASET(PARQUET_TO_CSV.out.transcripts_csv, 0.3) + BAYSOR_CREATE_DATASET(PARQUET2CSV.out.transcripts_csv, 0.3) // run baysor preview if param - generate_preview is true ch_sampled_transcripts = BAYSOR_CREATE_DATASET.out.sampled_transcripts @@ -38,12 +39,13 @@ workflow BAYSOR_GENERATE_PREVIEW { BAYSOR_PREVIEW(ch_baysor_preview_input) // clean the preview html file generated - EXTRACT_PREVIEW_DATA(BAYSOR_PREVIEW.out.preview_html) + EXTRACT_PREVIEW_DATA(BAYSOR_PREVIEW.out.html) ch_preview_mqc_html = EXTRACT_PREVIEW_DATA.out.mqc_data ch_preview_mqc_png = EXTRACT_PREVIEW_DATA.out.mqc_img emit: + preview_html = ch_preview_mqc_html // channel: [ val(meta), ["*_mqc.tsv"] ] preview_img = ch_preview_mqc_png // channel: [ val(meta), ["*_mqc.png"] ] } diff --git a/subworkflows/local/baysor_generate_preview/meta.yml b/subworkflows/local/baysor_generate_preview/meta.yml index 5807c233..c9a94529 100644 --- a/subworkflows/local/baysor_generate_preview/meta.yml +++ b/subworkflows/local/baysor_generate_preview/meta.yml @@ -12,7 +12,7 @@ keywords: components: - baysor/preview - baysor/create/dataset - - parquet/to/csv + - parquet2csv - extract/preview/data input: - ch_transcripts_parquet: diff --git a/subworkflows/local/baysor_generate_segfree/main.nf b/subworkflows/local/baysor_generate_segfree/main.nf index 74a160f0..4e8dbaa0 100644 --- a/subworkflows/local/baysor_generate_segfree/main.nf +++ b/subworkflows/local/baysor_generate_segfree/main.nf @@ -2,19 +2,21 @@ // Run baysor segfree // -include { BAYSOR_PREPROCESS_TRANSCRIPTS } from '../../../modules/local/baysor/preprocess/main' -include { BAYSOR_SEGFREE } from '../../../modules/local/baysor/segfree/main' -// include a module to process the output loom file with scapny or anndata +include { BAYSOR_SEGFREE } from '../../../modules/nf-core/baysor/segfree/main' + +include { BAYSOR_PREPROCESS_TRANSCRIPTS } from '../../../modules/local/utility/preprocess/main' +// TODO - should we include a module to process the output loom file with scapny or anndata? workflow BAYSOR_GENERATE_SEGFREE { + take: ch_transcripts_file // channel: [ val(meta), ["transcripts.parquet"] ] - ch_config // channel: [ ["path-to-xenium.toml"] ] - max_x // value: spatial filter upper x bound - max_y // value: spatial filter upper y bound - min_qv // value: minimum transcript QV - min_x // value: spatial filter lower x bound - min_y // value: spatial filter lower y bound + ch_config // channel: [ ["path-to-xenium.toml"] ] + max_x // value: spatial filter upper x bound + max_y // value: spatial filter upper y bound + min_qv // value: minimum transcript QV + min_x // value: spatial filter lower x bound + min_y // value: spatial filter lower y bound main: @@ -31,7 +33,7 @@ workflow BAYSOR_GENERATE_SEGFREE { max_y, min_y, ) - ch_transcripts = BAYSOR_PREPROCESS_TRANSCRIPTS.out.transcripts_file + ch_transcripts = BAYSOR_PREPROCESS_TRANSCRIPTS.out.transcripts_csv // run baysor segfree ch_baysor_segfree_input = ch_transcripts @@ -48,5 +50,6 @@ workflow BAYSOR_GENERATE_SEGFREE { ) emit: + ncvs = BAYSOR_SEGFREE.out.ncvs // channel: [ val(meta), ["ncvs.loom"] ] } diff --git a/subworkflows/local/baysor_generate_segfree/meta.yml b/subworkflows/local/baysor_generate_segfree/meta.yml index b312ff00..db8696aa 100644 --- a/subworkflows/local/baysor_generate_segfree/meta.yml +++ b/subworkflows/local/baysor_generate_segfree/meta.yml @@ -8,6 +8,7 @@ keywords: - loom components: - baysor/segfree + - baysor/preprocess/transcripts input: - ch_transcripts_parquet: description: | diff --git a/subworkflows/local/baysor_run_prior_segmentation_mask/main.nf b/subworkflows/local/baysor_run_prior_segmentation_mask/main.nf index d5acc0a1..c51eb79f 100644 --- a/subworkflows/local/baysor_run_prior_segmentation_mask/main.nf +++ b/subworkflows/local/baysor_run_prior_segmentation_mask/main.nf @@ -2,58 +2,77 @@ // Run baysor run & import-segmentation // -include { BAYSOR_PREPROCESS_TRANSCRIPTS } from '../../../modules/local/baysor/preprocess/main' -include { BAYSOR_RUN } from '../../../modules/local/baysor/run/main' +include { BAYSOR_RUN } from '../../../modules/nf-core/baysor/run/main' include { XENIUMRANGER_IMPORT_SEGMENTATION } from '../../../modules/nf-core/xeniumranger/import-segmentation/main' +include { BAYSOR_PREPROCESS_TRANSCRIPTS } from '../../../modules/local/utility/preprocess/main' +include { BAYSOR_ESTIMATE_SCALE_FACTOR } from '../../../modules/local/utility/estimatescalefactor/main' workflow BAYSOR_RUN_PRIOR_SEGMENTATION_MASK { + take: - ch_bundle_path // channel: [ val(meta), ["path-to-xenium-bundle"] ] - ch_transcripts_file // channel: [ val(meta), ["path-to-transcripts.parquet"] ] - ch_segmentation_mask // channel: [ ["path-to-prior-segmentation-mask"] ] - ch_config // channel: [ "path-to-xenium.toml" ] - max_x // value: spatial filter upper x bound - max_y // value: spatial filter upper y bound - min_qv // value: minimum transcript QV - min_x // value: spatial filter lower x bound - min_y // value: spatial filter lower y bound + ch_bundle_path // channel: [ val(meta), ["path-to-xenium-bundle"] ] + ch_transcripts_parquet // channel: [ val(meta), ["path-to-transcripts.parquet"] ] + ch_segmentation_mask // channel: [ ["path-to-prior-segmentation-mask"] ] + ch_config // channel: [ "path-to-xenium.toml" ] + max_x // value: spatial filter upper x bound + max_y // value: spatial filter upper y bound + min_qv // value: minimum transcript QV + min_x // value: spatial filter lower x bound + min_y // value: spatial filter lower y bound + ch_prior_column // channel: [val("cell_id")] + ch_prior_confidence // channel: [val(prior_confidence) ] + ch_transcripts_per_cell // channel: [val(min_transcripts_per_cell)] main: - ch_transcripts = channel.empty() - + ch_transcripts = channel.empty() ch_redefined_bundle = channel.empty() ch_coordinate_space = channel.value("pixels") + ch_x_column = channel.value("x_location") + ch_y_column = channel.value("y_location") + ch_polygon_format = channel.value("GeometryCollectionLegacy") // Always preprocess transcripts.parquet to CSV for Baysor 0.7.1 compatibility. // Baysor's Julia Parquet.jl cannot read zstd-compressed parquet files from Xenium bundles. // Also applies optional spatial/QV filtering when filter_transcripts is true. BAYSOR_PREPROCESS_TRANSCRIPTS( - ch_transcripts_file, + ch_transcripts_parquet, min_qv, max_x, min_x, max_y, min_y, ) - ch_transcripts = BAYSOR_PREPROCESS_TRANSCRIPTS.out.transcripts_file + ch_transcripts = BAYSOR_PREPROCESS_TRANSCRIPTS.out.transcripts_csv - // run baysor with prior segmentation mask - ch_baysor_input = ch_transcripts + // estimated scale factor cell radius + BAYSOR_ESTIMATE_SCALE_FACTOR ( + ch_transcripts_parquet, + ch_prior_column, + ch_x_column, + ch_y_column, + ch_transcripts_per_cell + ) + ch_scale_factor = BAYSOR_ESTIMATE_SCALE_FACTOR.out.scale_factor + + + // run baysor with prior segmentation mask with the estimated scale factor + ch_baysor_input = BAYSOR_PREPROCESS_TRANSCRIPTS.out.transcripts_csv .combine(ch_segmentation_mask) .combine(ch_config) - .map { meta, transcripts, mask, config -> + .combine(ch_scale_factor) + .map { meta, transcripts, mask, config, scale_factor -> tuple( meta, transcripts, mask, config, - 30, + scale_factor, ) } - BAYSOR_RUN(ch_baysor_input) + BAYSOR_RUN(ch_baysor_input, ch_prior_column, ch_prior_confidence, ch_polygon_format) // run import-segmentation with baysor outs @@ -74,10 +93,10 @@ workflow BAYSOR_RUN_PRIOR_SEGMENTATION_MASK { XENIUMRANGER_IMPORT_SEGMENTATION( ch_imp_seg_inputs ) - ch_redefined_bundle = XENIUMRANGER_IMPORT_SEGMENTATION.out.outs emit: + coordinate_space = ch_coordinate_space // channel: [ "pixels" ] redefined_bundle = ch_redefined_bundle // channel: [ val(meta), ["redefined-xenium-bundle"] ] } diff --git a/subworkflows/local/baysor_run_prior_segmentation_mask/meta.yml b/subworkflows/local/baysor_run_prior_segmentation_mask/meta.yml index f01f44c7..a4e5e38c 100644 --- a/subworkflows/local/baysor_run_prior_segmentation_mask/meta.yml +++ b/subworkflows/local/baysor_run_prior_segmentation_mask/meta.yml @@ -14,6 +14,7 @@ components: - baysor/preprocess/transcripts - baysor/run - xeniumranger/import/segmentation + - baysor/estimate/scale/factor input: - ch_bundle_path: description: | @@ -31,6 +32,38 @@ input: description: | config file for the xenium baysor run (stored in assets/config/xenium.toml) Structure: [ path("path-to-xenium.toml") ] + - max_x: + description: | + maximum x coordinate for spatial filtering of transcripts + Structure: [ val("max_x") ] + - max_y: + description: | + maximum y coordinate for spatial filtering of transcripts + Structure: [ val("max_y") ] + - min_qv: + description: | + minimum quality value for filtering transcripts + Structure: [ val("min_qv") ] + - min_x: + description: | + minimum x coordinate for spatial filtering of transcripts + Structure: [ val("min_x") ] + - min_y: + description: | + minimum y coordinate for spatial filtering of transcripts + Structure: [ val("min_y") ] + - ch_prior_column: + description: | + the column name in the prior segmentation mask to use as a prior for Baysor segmentation + Structure: [ val("prior_column") ] + - ch_prior_confidence: + description: | + the confidence value for the prior segmentation mask to use as a prior for segmentation + Structure: [ val("prior_confidence") ] + - ch_polygon_format: + description: | + the polygon format to use for Baysor segmentation output + Structure: [ val("GeometryCollectionLegacy") ] output: - segmentation: description: | diff --git a/subworkflows/local/baysor_run_transcripts_parquet/main.nf b/subworkflows/local/baysor_run_transcripts_parquet/main.nf index aff27051..a3d673ae 100644 --- a/subworkflows/local/baysor_run_transcripts_parquet/main.nf +++ b/subworkflows/local/baysor_run_transcripts_parquet/main.nf @@ -9,43 +9,62 @@ // Image-based (cellpose): non-tiled only (mask passed to Baysor) // +include { BAYSOR_RUN } from '../../../modules/nf-core/baysor/run/main' +include { XENIUMRANGER_IMPORT_SEGMENTATION } from '../../../modules/nf-core/xeniumranger/import-segmentation/main' + include { XENIUM_PATCH_DIVIDE } from '../../../modules/local/xenium_patch/divide/main' -include { PARQUET_TO_CSV } from '../../../modules/local/parquet_to_csv/main' -include { BAYSOR_RUN } from '../../../modules/local/baysor/run/main' -include { BAYSOR_PREPROCESS_TRANSCRIPTS } from '../../../modules/local/baysor/preprocess/main' include { XENIUM_PATCH_STITCH } from '../../../modules/local/xenium_patch/stitch/main' +include { PARQUET2CSV } from '../../../modules/local/utility/parquet2csv/main' +include { BAYSOR_PREPROCESS_TRANSCRIPTS } from '../../../modules/local/utility/preprocess/main' include { RECONSTRUCT_PATCHES } from '../../../modules/local/utility/reconstruct_patches/main' -include { XENIUMRANGER_IMPORT_SEGMENTATION } from '../../../modules/nf-core/xeniumranger/import-segmentation/main' +include { BAYSOR_ESTIMATE_SCALE_FACTOR } from '../../../modules/local/utility/estimatescalefactor/main' workflow BAYSOR_RUN_TRANSCRIPTS_PARQUET { take: - ch_bundle_path // channel: [ val(meta), ["path-to-xenium-bundle"] ] - ch_transcripts_file // channel: [ val(meta), ["transcripts.parquet"] ] - ch_morphology_image // channel: [ val(meta), ["morphology_focus.ome.tif"] ] - ch_config // channel: ["path-to-xenium.toml"] - ch_prior_mask // channel: [ val(meta), ["resized_mask.tif"] ] or empty (cellpose) - baysor_config // value: path to baysor config TOML (or null) - baysor_scale // value: Baysor --scale for non-tiled runs - baysor_tiling // value: bool — enable tiling - baysor_tiling_scale // value: Baysor --scale for tiled runs - max_x // value: spatial filter upper x bound - max_y // value: spatial filter upper y bound - min_qv // value: minimum transcript QV - min_x // value: spatial filter lower x bound - min_y // value: spatial filter lower y bound + ch_bundle_path // channel: [ val(meta), ["path-to-xenium-bundle"] ] + ch_transcripts_parquet // channel: [ val(meta), ["transcripts.parquet"] ] + ch_morphology_image // channel: [ val(meta), ["morphology_focus.ome.tif"] ] + ch_config // channel: ["path-to-xenium.toml"] + ch_prior_mask // channel: [ val(meta), ["resized_mask.tif"] ] or empty (cellpose) + baysor_tiling // value: bool — enable tiling + max_x // value: spatial filter upper x bound + max_y // value: spatial filter upper y bound + min_qv // value: minimum transcript QV + min_x // value: spatial filter lower x bound + min_y // value: spatial filter lower y bound + ch_prior_column // channel: [val("cell_id")] or empty (no prior) + ch_transcripts_per_cell // channel: [val(min_transcripts_per_cell)] main: + ch_x_column = channel.value("x_location") + ch_y_column = channel.value("y_location") ch_coordinate_space = channel.value("microns") + ch_polygon_format = channel.value("GeometryCollectionLegacy") - if ( baysor_tiling ) { + // Estimate scale factor which specified the cell radius for each patch + BAYSOR_ESTIMATE_SCALE_FACTOR ( + ch_transcripts_parquet, + ch_prior_column, + ch_x_column, + ch_y_column, + ch_transcripts_per_cell + ) + + // keep meta-keyed for non-tiled join + ch_scale_factor_by_meta = BAYSOR_ESTIMATE_SCALE_FACTOR.out.scale_factor - // ── TILED PATH ────────────────────────────────────────────────── + // keyed by sample_id (String) for the tiled/patch path + ch_scale_factor_by_sample = ch_scale_factor_by_meta + .map { meta, scale_factor -> tuple(meta.id, scale_factor) } + + // ── TILED PATH ─────────────────────── + if ( baysor_tiling ) { // Step 1: Divide transcripts into overlapping patches - ch_divide_input = ch_transcripts_file + ch_divide_input = ch_transcripts_parquet .join(ch_morphology_image, by: 0) XENIUM_PATCH_DIVIDE ( ch_divide_input ) @@ -63,16 +82,19 @@ workflow BAYSOR_RUN_TRANSCRIPTS_PARQUET { } // Step 2b: Convert parquet to CSV (Baysor Julia Parquet.jl incompatibility) - PARQUET_TO_CSV ( ch_patches ) + PARQUET2CSV ( ch_patches ) // Step 3: Run Baysor on each patch independently // Use baysor_tiling_scale (larger than baysor_scale) to compensate for EM // convergence producing smaller cells on tile-sized datasets. - BAYSOR_RUN ( - PARQUET_TO_CSV.out.csv.map { meta, transcripts -> - tuple(meta, transcripts, [], baysor_config ? file(baysor_config) : [], baysor_tiling_scale) + ch_baysor_input = PARQUET2CSV.out.transcripts_csv + .map { meta, csv -> tuple(meta.sample_id, meta, csv) } + .combine(ch_config) + .combine(ch_scale_factor_by_sample, by: 0) + .map { _sample_id, meta, transcripts, config, scale_factor -> + tuple(meta, transcripts, [], config ? file(config) : [], scale_factor) } - ) + BAYSOR_RUN (ch_baysor_input, [], [], ch_polygon_format) // Step 4: Gather patch results per sample and reconstruct patches directory ch_baysor_results = BAYSOR_RUN.out.segmentation @@ -124,7 +146,7 @@ workflow BAYSOR_RUN_TRANSCRIPTS_PARQUET { // Preprocess: parquet → CSV with optional spatial/QV filtering BAYSOR_PREPROCESS_TRANSCRIPTS( - ch_transcripts_file, + ch_transcripts_parquet, min_qv, max_x, min_x, @@ -133,17 +155,18 @@ workflow BAYSOR_RUN_TRANSCRIPTS_PARQUET { ) // Run Baysor on full transcripts (with optional image-based prior mask) - ch_csv_with_mask = BAYSOR_PREPROCESS_TRANSCRIPTS.out.transcripts_file + ch_csv_with_mask = BAYSOR_PREPROCESS_TRANSCRIPTS.out.transcripts_csv .join(ch_prior_mask, by: 0, remainder: true) .map { meta, transcripts, mask -> tuple(meta, transcripts, mask ?: []) } ch_baysor_input = ch_csv_with_mask .combine(ch_config) - .map { meta, transcripts, mask, config -> - tuple(meta, transcripts, mask, config, baysor_scale) + .combine(ch_scale_factor_by_meta, by: 0) + .map { meta, transcripts, mask, config, scale_factor -> + tuple(meta, transcripts, mask, config, scale_factor) } - BAYSOR_RUN(ch_baysor_input) + BAYSOR_RUN(ch_baysor_input, [], [], ch_polygon_format) // xeniumranger import-segmentation (non-tiled) // spatialaxe signature: meta, bundle, transcript_assignment, viz_polygons, nuclei, cells, coordinate_transform, units @@ -156,11 +179,11 @@ workflow BAYSOR_RUN_TRANSCRIPTS_PARQUET { [], [], [], ch_coordinate_space.val) } - XENIUMRANGER_IMPORT_SEGMENTATION(ch_xr) } emit: + redefined_bundle = XENIUMRANGER_IMPORT_SEGMENTATION.out.outs coordinate_space = ch_coordinate_space } diff --git a/subworkflows/local/baysor_run_transcripts_parquet/meta.yml b/subworkflows/local/baysor_run_transcripts_parquet/meta.yml index ccc4a816..f3cd8f5f 100644 --- a/subworkflows/local/baysor_run_transcripts_parquet/meta.yml +++ b/subworkflows/local/baysor_run_transcripts_parquet/meta.yml @@ -11,9 +11,14 @@ keywords: - polygons components: - baysor/preprocess/transcripts + - baysor/estimate/scale/factor - baysor/run - xeniumranger/import/segmentation - split/transcripts + - parquet2csv + - reconstruct/patches + - xenium/patch/divide + - xenium/patch/stitch input: - ch_bundle_path: description: | @@ -23,10 +28,54 @@ input: description: | input parquet file from the xenium bundle Structure: [ val(meta), path("path-to-transcripts.parquet") ] + - ch_morphology_image: + description: | + the morphology_focus.ome.tif file from the xenium bundle + Structure: [ val(meta), path("morphology_focus.ome.tif") ] - ch_config: description: | config file for the xenium baysor run (stored in assets/config/xenium.toml) Structure: [ path("path-to-xenium.toml") ] + - ch_prior_mask: + description: | + the resized_mask.tif file from the xenium bundle (or empty for cellpose) + Structure: [ val(meta), path("resized_mask.tif") ] or empty + - baysor_scale: + description: | + the --scale parameter for the baysor run command (for non-tiled runs) + Structure: [ val("integer") ] + - baysor_tiling: + description: | + whether to enable tiling for the baysor run command + Structure: [ val("boolean") ] + - baysor_tiling_scale: + description: | + the --scale parameter for the baysor run command (for tiled runs) + Structure: [ val("integer") ] + - max_x: + description: | + maximum x coordinate for spatial filtering of transcripts + Structure: [ val("integer") ] + - max_y: + description: | + maximum y coordinate for spatial filtering of transcripts + Structure: [ val("integer") ] + - min_qv: + description: | + minimum QV for spatial filtering of transcripts + Structure: [ val("integer") ] + - min_x: + description: | + minimum x coordinate for spatial filtering of transcripts + Structure: [ val("integer") ] + - min_y: + description: | + minimum y coordinate for spatial filtering of transcripts + Structure: [ val("integer") ] + - ch_polygon_format: + description: | + the polygon format for the baysor run command + Structure: [ val("GeometryCollectionLegacy") ] output: - segmentation: description: | diff --git a/subworkflows/local/baysor_run_transcripts_parquet_tiled/main.nf b/subworkflows/local/baysor_run_transcripts_parquet_tiled/main.nf index 70045331..34e2610c 100644 --- a/subworkflows/local/baysor_run_transcripts_parquet_tiled/main.nf +++ b/subworkflows/local/baysor_run_transcripts_parquet_tiled/main.nf @@ -2,30 +2,37 @@ // Runs baysor with tiling: divide transcripts -> preprocess per patch -> baysor per patch -> stitch -> xeniumranger // +include { BAYSOR_RUN } from '../../../modules/nf-core/baysor/run/main' +include { XENIUMRANGER_IMPORT_SEGMENTATION } from '../../../modules/nf-core/xeniumranger/import-segmentation/main' + include { XENIUM_PATCH_DIVIDE } from '../../../modules/local/xenium_patch/divide/main' -include { BAYSOR_PREPROCESS_TRANSCRIPTS } from '../../../modules/local/baysor/preprocess/main' -include { BAYSOR_RUN } from '../../../modules/local/baysor/run/main' +include { BAYSOR_PREPROCESS_TRANSCRIPTS } from '../../../modules/local/utility/preprocess/main' include { XENIUM_PATCH_STITCH } from '../../../modules/local/xenium_patch/stitch/main' -include { XENIUMRANGER_IMPORT_SEGMENTATION } from '../../../modules/nf-core/xeniumranger/import-segmentation/main' +include { BAYSOR_ESTIMATE_SCALE_FACTOR } from '../../../modules/local/utility/estimatescalefactor/main' workflow BAYSOR_RUN_TRANSCRIPTS_PARQUET_TILED { take: - ch_bundle_path // channel: [ val(meta), ["xenium-bundle"] ] - ch_transcripts_file // channel: [ val(meta), ["transcripts.parquet"] ] - ch_config // channel: ["path-to-xenium.toml"] - max_x // value: spatial filter upper x bound - max_y // value: spatial filter upper y bound - min_qv // value: minimum transcript QV - min_x // value: spatial filter lower x bound - min_y // value: spatial filter lower y bound + ch_bundle_path // channel: [ val(meta), ["xenium-bundle"] ] + ch_transcripts_parquet // channel: [ val(meta), ["transcripts.parquet"] ] + ch_config // channel: ["path-to-xenium.toml"] + max_x // value: spatial filter upper x bound + max_y // value: spatial filter upper y bound + min_qv // value: minimum transcript QV + min_x // value: spatial filter lower x bound + min_y // value: spatial filter lower y bound + ch_prior_column // channel: [val("cell_id")] + ch_x_column // channel: [val("x_location")] + ch_y_column // channel: [val("y_location")] + ch_transcripts_per_cell // channel: [val(min_transcripts_per_cell)] main: ch_coordinate_space = channel.value("microns") + ch_polygon_format = channel.value("GeometryCollectionLegacy") // Step 1: Divide transcripts into overlapping patches - XENIUM_PATCH_DIVIDE ( ch_transcripts_file ) + XENIUM_PATCH_DIVIDE ( ch_transcripts_parquet ) // Step 2: Fan out patches for parallel processing ch_patches = XENIUM_PATCH_DIVIDE.out.patch_transcripts @@ -47,17 +54,28 @@ workflow BAYSOR_RUN_TRANSCRIPTS_PARQUET_TILED { max_x, min_x, max_y, - min_y, + min_y ) + // Step 4: // estimate scale factor which specifed the cell radius for each patch + BAYSOR_ESTIMATE_SCALE_FACTOR ( + ch_transcripts_parquet, + ch_prior_column, + ch_x_column, + ch_y_column, + ch_transcripts_per_cell + ) + ch_scale_factor = BAYSOR_ESTIMATE_SCALE_FACTOR.out.scale_factor + // Step 4: Run Baysor on each patch independently - ch_baysor_input = BAYSOR_PREPROCESS_TRANSCRIPTS.out.transcripts_file + ch_baysor_input = BAYSOR_PREPROCESS_TRANSCRIPTS.out.transcripts_csv .combine(ch_config) - .map { meta, transcripts, config -> - tuple(meta, transcripts, [], config, 30) + .combine(ch_scale_factor) + .map { meta, transcripts, config, scale_factor -> + tuple(meta, transcripts, [], config, scale_factor) } - BAYSOR_RUN ( ch_baysor_input ) + BAYSOR_RUN ( ch_baysor_input, [], [], ch_polygon_format ) // Step 5: Gather patch results per sample for stitching ch_for_stitch = BAYSOR_RUN.out.segmentation @@ -99,6 +117,7 @@ workflow BAYSOR_RUN_TRANSCRIPTS_PARQUET_TILED { XENIUMRANGER_IMPORT_SEGMENTATION ( ch_xr ) emit: + coordinate_space = ch_coordinate_space // channel: [ "microns" ] redefined_bundle = XENIUMRANGER_IMPORT_SEGMENTATION.out.outs // channel: [ val(meta), ["redefined-xenium-bundle"] ] } diff --git a/subworkflows/local/baysor_run_transcripts_parquet_tiled/meta.yml b/subworkflows/local/baysor_run_transcripts_parquet_tiled/meta.yml new file mode 100644 index 00000000..e3b8cb06 --- /dev/null +++ b/subworkflows/local/baysor_run_transcripts_parquet_tiled/meta.yml @@ -0,0 +1,81 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/subworkflows/yaml-schema.json +name: "baysor_run_transcripts_parquet_tiled" +description: + to run the `baysor run` command with the transcripts.parquet file as a coordinate-based segmentation run in a tiled manner, + where the input transcripts.parquet file is divided into smaller tiles and processed in parallel +keywords: + - baysor + - baysor run + - segmentation + - xeniumranger import-segmentation + - coordinate-based segmentation + - transcript filtering + - polygons +components: + - baysor/preprocess/transcripts + - baysor/run + - xeniumranger/import/segmentation + - split/transcripts + - parquet2csv + - reconstruct/patches + - xenium/patch/divide + - xenium/patch/stitch + - baysor/estimate/scale/factor +input: + - ch_bundle_path: + description: | + path to the xenium bundle + Structure: [ val(meta), path("path-to-xenium-bundle") ] + - ch_transcripts_parquet: + description: | + input parquet file from the xenium bundle + Structure: [ val(meta), path("path-to-transcripts.parquet") ] + - ch_config: + description: | + config file for the xenium baysor run (stored in assets/config/xenium.toml) + Structure: [ path("path-to-xenium.toml") ] + - max_x: + description: | + maximum x coordinate for spatial filtering of transcripts + Structure: [ val("max-x-coordinate") ] + - max_y: + description: | + maximum y coordinate for spatial filtering of transcripts + Structure: [ val("max-y-coordinate") ] + - min_x: + description: | + minimum x coordinate for spatial filtering of transcripts + Structure: [ val("min-x-coordinate") ] + - min_y: + description: | + minimum y coordinate for spatial filtering of transcripts + Structure: [ val("min-y-coordinate") ] + - ch_prior_column: + description: | + the column name in the prior segmentation mask to use as a prior for Baysor segmentation + Structure: [ val("prior-column") ] + - ch_x_column: + description: | + the column name in the transcripts.parquet file that contains the x coordinates of the transcripts + Structure: [ val("x_location") ] + - ch_y_column: + description: | + the column name in the transcripts.parquet file that contains the y coordinates of the transcripts + Structure: [ val("y_location") ] + - ch_transcripts_per_cell: + description: | + min number of transcripts per cell for filtering transcripts + Structure: [ val("min-transcripts-per-cell") ] +output: + - coordinate_space: + description: | + the coordinate space in which xeniumranger import-segmentation was run + Structure: [ val("microns") ] + - redefined_bundle: + description: | + the redefined xenium bundle generated with the segmentation results from baysor + Structure: [ val(meta), ["redefined-xenium-bundle"] ] +authors: + - "@khersameesh24" +maintainers: + - "@khersameesh24" diff --git a/subworkflows/local/cellpose_baysor_import_segmentation/main.nf b/subworkflows/local/cellpose_baysor_import_segmentation/main.nf index 38bbcc74..d8ae85eb 100644 --- a/subworkflows/local/cellpose_baysor_import_segmentation/main.nf +++ b/subworkflows/local/cellpose_baysor_import_segmentation/main.nf @@ -2,22 +2,23 @@ // Run the cellpose, baysor and import-segmentation flow // -include { RESOLIFT } from '../../../modules/local/resolift/main' -include { BAYSOR_RUN } from '../../../modules/local/baysor/run/main' +include { BAYSOR_RUN } from '../../../modules/nf-core/baysor/run/main' include { CELLPOSE as CELLPOSE_CELLS } from '../../../modules/nf-core/cellpose/main' -include { EXTRACT_DAPI } from '../../../modules/local/utility/extract_dapi/main' include { STARDIST as STARDIST_NUCLEI } from '../../../modules/nf-core/stardist/main' -include { CONVERT_MASK_UINT32 } from '../../../modules/local/utility/convert_mask_uint32/main' -include { BAYSOR_PREPROCESS_TRANSCRIPTS } from '../../../modules/local/baysor/preprocess/main' -include { RESIZE_TIF } from '../../../modules/local/utility/resize_tif/main' -include { GET_TRANSCRIPTS_COORDINATES } from '../../../modules/local/utility/get_coordinates/main' include { XENIUMRANGER_IMPORT_SEGMENTATION } from '../../../modules/nf-core/xeniumranger/import-segmentation/main' +include { RESOLIFT } from '../../../modules/local/resolift/main' +include { BAYSOR_PREPROCESS_TRANSCRIPTS } from '../../../modules/local/utility/preprocess/main' +include { RESIZE_TIF } from '../../../modules/local/utility/resize_tif/main' +include { EXTRACT_DAPI } from '../../../modules/local/utility/extract_dapi/main' +include { CONVERT_MASK_UINT32 } from '../../../modules/local/utility/convert_mask_uint32/main' +include { BAYSOR_ESTIMATE_SCALE_FACTOR } from '../../../modules/local/utility/estimatescalefactor/main' + workflow CELLPOSE_BAYSOR_IMPORT_SEGMENTATION { take: ch_morphology_image // channel: [ val(meta), ["path-to-morphology.ome.tif"] ] ch_bundle_path // channel: [ val(meta), ["path-to-xenium-bundle"] ] - ch_transcripts_file // channel: [ val(meta), ["path-to-transcripts.parquet"] ] + ch_transcripts_parquet // channel: [ val(meta), ["path-to-transcripts.parquet"] ] ch_experiment_metadata // channel: [ val(meta), ["path-to-experiment.xenium"] ] ch_config // channel: ["path-to-xenium.toml"] cell_segmentation_only // value: bool @@ -27,6 +28,8 @@ workflow CELLPOSE_BAYSOR_IMPORT_SEGMENTATION { min_qv // value: minimum transcript QV min_x // value: spatial filter lower x bound min_y // value: spatial filter lower y bound + ch_prior_column // value: [val("cell_id")] + ch_transcripts_per_cell // value: [val(min_transcripts_per_cell)] nucleus_segmentation_only // value: bool sharpen_tiff // value: bool stardist_nuclei_model // value: stardist pretrained model name @@ -36,6 +39,9 @@ workflow CELLPOSE_BAYSOR_IMPORT_SEGMENTATION { ch_transcripts = channel.empty() ch_imp_seg_inputs = channel.empty() ch_coordinate_space = channel.value("microns") + ch_x_column = channel.value("x_location") + ch_y_column = channel.value("y_location") + ch_polygon_format = channel.value("GeometryCollectionLegacy") // Use empty list when no model is provided; path input for official cellpose module @@ -76,14 +82,24 @@ workflow CELLPOSE_BAYSOR_IMPORT_SEGMENTATION { // Baysor's Julia Parquet.jl cannot read zstd-compressed parquet files from Xenium bundles. // Also applies optional spatial/QV filtering when filter_transcripts is true. BAYSOR_PREPROCESS_TRANSCRIPTS( - ch_transcripts_file, + ch_transcripts_parquet, min_qv, max_x, min_x, max_y, min_y, ) - ch_transcripts = BAYSOR_PREPROCESS_TRANSCRIPTS.out.transcripts_file + ch_transcripts = BAYSOR_PREPROCESS_TRANSCRIPTS.out.transcripts_csv + + // estimate scale factor which specifed the cell radius for baysor run + BAYSOR_ESTIMATE_SCALE_FACTOR ( + ch_transcripts_parquet, + ch_prior_column, + ch_x_column, + ch_y_column, + ch_transcripts_per_cell + ) + ch_scale_factor = BAYSOR_ESTIMATE_SCALE_FACTOR.out.scale_factor // run baysor with cellpose results @@ -107,16 +123,17 @@ workflow CELLPOSE_BAYSOR_IMPORT_SEGMENTATION { ch_baysor_input = ch_transcripts .combine(RESIZE_TIF.out.resized_mask, by: 0) .combine(ch_config) - .map { meta, transcripts, mask, config -> + .combine(ch_scale_factor) + .map { meta, transcripts, mask, config, scale_factor -> tuple( meta, transcripts, mask, config, - 30, + scale_factor, ) } - BAYSOR_RUN(ch_baysor_input) + BAYSOR_RUN(ch_baysor_input, [], [], ch_polygon_format) } else if (cell_segmentation_only) { @@ -138,32 +155,34 @@ workflow CELLPOSE_BAYSOR_IMPORT_SEGMENTATION { ch_baysor_input = ch_transcripts .combine(RESIZE_TIF.out.resized_mask, by: 0) .combine(ch_config) - .map { meta, transcripts, mask, config -> + .combine(ch_scale_factor) + .map { meta, transcripts, mask, config, scale_factor -> tuple( meta, transcripts, mask, config, - 30, + scale_factor, ) } - BAYSOR_RUN(ch_baysor_input) + BAYSOR_RUN(ch_baysor_input, [], [], ch_polygon_format) } else { // run baysor without cell/nuclei mask ch_baysor_input = ch_transcripts .combine(ch_config) - .map { meta, transcripts, config -> + .combine(ch_scale_factor) + .map { meta, transcripts, config, scale_factor -> tuple( meta, transcripts, [], config, - 30, + scale_factor, ) } - BAYSOR_RUN(ch_baysor_input) + BAYSOR_RUN(ch_baysor_input, [], [], ch_polygon_format) } @@ -186,6 +205,7 @@ workflow CELLPOSE_BAYSOR_IMPORT_SEGMENTATION { XENIUMRANGER_IMPORT_SEGMENTATION(ch_imp_seg_inputs) emit: - coordinate_space = ch_coordinate_space // channel: [ val("microns") ] + + coordinate_space = ch_coordinate_space // channel: [ val("microns") ] redefined_bundle = XENIUMRANGER_IMPORT_SEGMENTATION.out.outs // channel: [ val(meta), ["redefined-xenium-bundle"] ] } diff --git a/subworkflows/local/cellpose_baysor_import_segmentation/meta.yml b/subworkflows/local/cellpose_baysor_import_segmentation/meta.yml index 0c18e495..327eae1e 100644 --- a/subworkflows/local/cellpose_baysor_import_segmentation/meta.yml +++ b/subworkflows/local/cellpose_baysor_import_segmentation/meta.yml @@ -19,9 +19,13 @@ components: - resize/tif - get/transcripts/coordinates - baysor/preprocess/transcripts + - baysor/estimate/scale/factor - baysor/run - xeniumranger/import/segmentation - split/transcripts + - convert/mask/uint32 + - extract/dapi + - stardist input: - ch_morphology_image: description: | diff --git a/subworkflows/local/cellpose_resolift_morphology_ome_tif/meta.yml b/subworkflows/local/cellpose_resolift_morphology_ome_tif/meta.yml index b0c99c85..7b18c2e2 100644 --- a/subworkflows/local/cellpose_resolift_morphology_ome_tif/meta.yml +++ b/subworkflows/local/cellpose_resolift_morphology_ome_tif/meta.yml @@ -11,6 +11,11 @@ components: - cellpose - resolift - xeniumranger/import/segmentation + - convert/mask/uint32 + - downscale/morphology + - extract/dapi + - stardist + - upscale/mask input: - ch_morphology_image: description: | diff --git a/subworkflows/local/ficture_preprocess_model/main.nf b/subworkflows/local/ficture_preprocess_model/main.nf index c45713ec..9090d9a3 100644 --- a/subworkflows/local/ficture_preprocess_model/main.nf +++ b/subworkflows/local/ficture_preprocess_model/main.nf @@ -4,23 +4,23 @@ include { FICTURE_PREPROCESS } from '../../../modules/local/ficture/preprocess/main' include { FICTURE } from '../../../modules/local/ficture/model/main' -include { PARQUET_TO_CSV } from '../../../modules/local/utility/parquet_to_csv/main' +include { PARQUET2CSV } from '../../../modules/local/utility/parquet2csv/main' workflow FICTURE_PREPROCESS_MODEL { take: - ch_transcripts_file // channel: [ val(meta), [ "transcripts.parquet" ] ] + ch_transcripts_parquet // channel: [ val(meta), [ "transcripts.parquet" ] ] ch_features // channel: [ ["features"] ] features // value: path to features list (or null) main: // convert parquet to csv - PARQUET_TO_CSV(ch_transcripts_file, ".csv") + PARQUET2CSV(ch_transcripts_parquet) // run ficture preprocessing - ch_transcripts = PARQUET_TO_CSV.out.transcripts_csv + ch_transcripts = PARQUET2CSV.out.transcripts_csv FICTURE_PREPROCESS(ch_transcripts, ch_features) @@ -32,8 +32,8 @@ workflow FICTURE_PREPROCESS_MODEL { ch_features_clean, ) emit: - transcripts = FICTURE_PREPROCESS.out.transcripts // channel: [ val(meta), [ "*processed_transcripts.tsv.gz" ] ] + transcripts = FICTURE_PREPROCESS.out.transcripts // channel: [ val(meta), [ "*processed_transcripts.tsv.gz" ] ] coordinate_minmax = FICTURE_PREPROCESS.out.coordinate_minmax // channel: [ "*coordinate_minmax.tsv" ] - features = FICTURE_PREPROCESS.out.features // channel: [ "*feature.clean.tsv.gz" ] - results = FICTURE.out.results // channel: [ val(meta), [ "results/** ] ] + features = FICTURE_PREPROCESS.out.features // channel: [ "*feature.clean.tsv.gz" ] + results = FICTURE.out.results // channel: [ val(meta), [ "results/** ] ] } diff --git a/subworkflows/local/ficture_preprocess_model/meta.yml b/subworkflows/local/ficture_preprocess_model/meta.yml index 17840559..62dd42e0 100644 --- a/subworkflows/local/ficture_preprocess_model/meta.yml +++ b/subworkflows/local/ficture_preprocess_model/meta.yml @@ -9,7 +9,7 @@ keywords: components: - ficture/preprocess - ficture - - parquet/to/csv + - parquet2csv input: - ch_transcripts_parquet: description: | diff --git a/subworkflows/local/opt_flip_track_stat/meta.yml b/subworkflows/local/opt_flip_track_stat/meta.yml deleted file mode 100644 index ea17599b..00000000 --- a/subworkflows/local/opt_flip_track_stat/meta.yml +++ /dev/null @@ -1,45 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/subworkflows/yaml-schema.json -name: "opt_flip_track_stat" -description: opt is a simple program that aligns probe sequences to transcript sequences - to detect potential off-target probe activity -keywords: - - opt - - flip - - track - - stat - - off-target probes -components: - - opt/flip - - opt/track - - opt/stat -input: - - ch_probe_fasta: - type: file - description: | - Input channel containing the sample info. and associated probe panel sequences fasta file - Structure: [ val(meta), path("porbe_panel_seqeunces.fasta") ] - pattern: "*.fasta" - - ch_references: - type: file - description: | - Input channel containing the sample info. and the references to be used - Structure: [ val(meta), path("reference_annotations.gff"), path("reference_annotations.fa") ] - pattern: "*.{fa,gff}" - - ch_gene_synonyms: - type: file - description: | - Input channel containing the Gene synonyms that may have been counted as off-targets but - simply differ in name (optional input) - Structure: [ val(meta), path("gene_synonyms.csv") ] - pattern: "*.csv" -output: - - summary: - type: file - description: | - Groovy Map containing summary of the forward oriented probes generated with the panel sequences opt flip and track - Structure: [ val(meta), path("collapsed_summary.tsv") ] - pattern: "*.tsv" -authors: - - "@khersameesh24" -maintainers: - - "@khersameesh24" diff --git a/subworkflows/local/proseg_preset_proseg2baysor/main.nf b/subworkflows/local/proseg_preset_proseg2baysor/main.nf index 3f9d8c99..47020bc6 100644 --- a/subworkflows/local/proseg_preset_proseg2baysor/main.nf +++ b/subworkflows/local/proseg_preset_proseg2baysor/main.nf @@ -2,22 +2,24 @@ // Runs proseg for the xenium format and proseg2baysor to generate cell ploygons // -include { PROSEG } from '../../../modules/local/proseg/preset/main' -include { PROSEG2BAYSOR } from '../../../modules/local/proseg/proseg2baysor/main' +include { PROSEG } from '../../../modules/nf-core/proseg/proseg/main' +include { PROSEG2BAYSOR } from '../../../modules/nf-core/proseg/proseg2baysor/main' + include { XENIUMRANGER_IMPORT_SEGMENTATION } from '../../../modules/nf-core/xeniumranger/import-segmentation/main' workflow PROSEG_PRESET_PROSEG2BAYSOR { take: ch_bundle_path // channel: [ val(meta), ["path-to-xenium-bundle"] ] - ch_transcripts_file // channel: [ val(meta), [ "transcripts.parquet" ] ] + ch_transcripts_parquet // channel: [ val(meta), [ "transcripts.parquet" ] ] main: ch_coordinate_space = channel.value("microns") + ch_proseg_mode = channel.value("xenium") + ch_expected_fmt = channel.value(["csv.gz", "csv.gz", "csv.gz"]) // run proseg with the xenium format - PROSEG(ch_transcripts_file) - + PROSEG(ch_transcripts_parquet, ch_proseg_mode, ch_expected_fmt) // run proseg-to-baysor on the zarr output from proseg v3 PROSEG2BAYSOR(PROSEG.out.zarr) @@ -25,8 +27,8 @@ workflow PROSEG_PRESET_PROSEG2BAYSOR { // run xeniumranger import-segmentation ch_imp_seg_inputs = ch_bundle_path - .combine(PROSEG2BAYSOR.out.xr_metadata, by: 0) - .combine(PROSEG2BAYSOR.out.xr_polygons, by: 0) + .combine(PROSEG2BAYSOR.out.transcript_metadata, by: 0) + .combine(PROSEG2BAYSOR.out.cell_polygons, by: 0) .map { meta, bundle, metadata, polygons2d -> tuple( meta, @@ -39,12 +41,10 @@ workflow PROSEG_PRESET_PROSEG2BAYSOR { ch_coordinate_space.val, ) } - - XENIUMRANGER_IMPORT_SEGMENTATION( - ch_imp_seg_inputs - ) + XENIUMRANGER_IMPORT_SEGMENTATION(ch_imp_seg_inputs) emit: - coordinate_space = ch_coordinate_space // channel: [ "microns" ] + + coordinate_space = ch_coordinate_space // channel: [ "microns" ] redefined_bundle = XENIUMRANGER_IMPORT_SEGMENTATION.out.outs // channel: [ val(meta), ["redefined-xenium-bundle"] ] } diff --git a/subworkflows/local/proseg_preset_proseg2baysor_tiled/main.nf b/subworkflows/local/proseg_preset_proseg2baysor_tiled/main.nf index 7ff6b783..0eed1221 100644 --- a/subworkflows/local/proseg_preset_proseg2baysor_tiled/main.nf +++ b/subworkflows/local/proseg_preset_proseg2baysor_tiled/main.nf @@ -2,24 +2,27 @@ // Runs proseg with tiling: divide transcripts -> proseg per patch -> proseg2baysor -> stitch -> xeniumranger // -include { XENIUM_PATCH_DIVIDE } from '../../../modules/local/xenium_patch/divide/main' -include { PROSEG } from '../../../modules/local/proseg/preset/main' -include { PROSEG2BAYSOR } from '../../../modules/local/proseg/proseg2baysor/main' -include { XENIUM_PATCH_STITCH } from '../../../modules/local/xenium_patch/stitch/main' +include { PROSEG } from '../../../modules/nf-core/proseg/proseg/main' +include { PROSEG2BAYSOR } from '../../../modules/nf-core/proseg/proseg2baysor/main' include { XENIUMRANGER_IMPORT_SEGMENTATION } from '../../../modules/nf-core/xeniumranger/import-segmentation/main' +include { XENIUM_PATCH_STITCH } from '../../../modules/local/xenium_patch/stitch/main' +include { XENIUM_PATCH_DIVIDE } from '../../../modules/local/xenium_patch/divide/main' + workflow PROSEG_PRESET_PROSEG2BAYSOR_TILED { take: ch_bundle_path // channel: [ val(meta), ["path-to-xenium-bundle"] ] - ch_transcripts_file // channel: [ val(meta), [ "transcripts.parquet" ] ] + ch_transcripts_parquet // channel: [ val(meta), [ "transcripts.parquet" ] ] main: ch_coordinate_space = channel.value("microns") + ch_proseg_mode = channel.value("xenium") + ch_expected_fmt = channel.value(["csv.gz", "csv.gz", "csv.gz"]) // Step 1: Divide transcripts into overlapping patches - XENIUM_PATCH_DIVIDE ( ch_transcripts_file ) + XENIUM_PATCH_DIVIDE ( ch_transcripts_parquet ) // Step 2: Fan out patches for parallel processing // transpose() emits one item per patch file: [meta, parquet_path] @@ -35,14 +38,14 @@ workflow PROSEG_PRESET_PROSEG2BAYSOR_TILED { } // Step 3: Run proseg on each patch independently - PROSEG ( ch_patches ) + PROSEG(ch_patches, ch_proseg_mode, ch_expected_fmt) // Step 4: Convert proseg output to baysor format per patch PROSEG2BAYSOR ( PROSEG.out.zarr ) // Step 5: Gather patch results per sample for stitching - ch_for_stitch = PROSEG2BAYSOR.out.xr_polygons - .join(PROSEG2BAYSOR.out.xr_metadata, by: 0) + ch_for_stitch = PROSEG2BAYSOR.out.cell_polygons + .join(PROSEG2BAYSOR.out.transcript_metadata, by: 0) .map { patch_meta, geojson, csv -> tuple(patch_meta.sample_id, [patch_meta.patch_id, csv, geojson]) } @@ -81,6 +84,7 @@ workflow PROSEG_PRESET_PROSEG2BAYSOR_TILED { XENIUMRANGER_IMPORT_SEGMENTATION ( ch_xr ) emit: + coordinate_space = ch_coordinate_space // channel: [ "microns" ] redefined_bundle = XENIUMRANGER_IMPORT_SEGMENTATION.out.outs // channel: [ val(meta), ["redefined-xenium-bundle"] ] } diff --git a/subworkflows/local/proseg_preset_proseg2baysor_tiled/meta.yml b/subworkflows/local/proseg_preset_proseg2baysor_tiled/meta.yml new file mode 100644 index 00000000..69e0eadf --- /dev/null +++ b/subworkflows/local/proseg_preset_proseg2baysor_tiled/meta.yml @@ -0,0 +1,39 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/subworkflows/yaml-schema.json +name: "proseg_preset_proseg2baysor_tiled" +description: to run proseg with the transcripts.parquet file as a coordinate-based segmentation run in a tiled manner + where the input transcripts.parquet file is divided into smaller tiles and processed in parallel +keywords: + - proseg + - segmentation + - xeniumranger import-segmentation + - coordinate-based segmentation + - polygons + - metadata +components: + - proseg + - proseg2baysor + - xenium/patch/divide + - xenium/patch/stitch + - xeniumranger/import/segmentation +input: + - ch_bundle_path: + description: | + path to the xenium bundle + Structure: [ val(meta), path("path-to-xenium-bundle") ] + - ch_transcripts_parquet: + description: | + input parquet file from the xenium bundle + Structure: [ val(meta), path("path-to-transcripts.parquet") ] +output: + - coordinate_space: + description: | + the coordinate space in which xeniumranger import-segmentation was run + Structure: [ val("microns") ] + - redefined_bundle: + description: | + the redefined xenium bundle generated with the segmentation results from baysor + Structure: [ val(meta), ["redefined-xenium-bundle"] ] +authors: + - "@khersameesh24" +maintainers: + - "@khersameesh24" diff --git a/subworkflows/local/segger_create_train_predict/main.nf b/subworkflows/local/segger_create_train_predict/main.nf index 3f832d61..2a2904a7 100644 --- a/subworkflows/local/segger_create_train_predict/main.nf +++ b/subworkflows/local/segger_create_train_predict/main.nf @@ -1,5 +1,5 @@ // -// Run segger create_dataset, train and predict modules & parquet_to_csv +// Run segger create_dataset, train and predict modules // include { SEGGER2XR } from '../../../modules/local/utility/segger2xr/main' @@ -72,6 +72,7 @@ workflow SEGGER_CREATE_TRAIN_PREDICT { ) emit: - coordinate_space = ch_coordinate_space // channel: [ "microns" ] + + coordinate_space = ch_coordinate_space // channel: [ "microns" ] redefined_bundle = XENIUMRANGER_IMPORT_SEGMENTATION.out.outs // channel: [ val(meta), ["redefined-xenium-bundle"] ] } diff --git a/subworkflows/local/stardist_resolift_morphology_ome_tif/meta.yml b/subworkflows/local/stardist_resolift_morphology_ome_tif/meta.yml new file mode 100644 index 00000000..a5e7648c --- /dev/null +++ b/subworkflows/local/stardist_resolift_morphology_ome_tif/meta.yml @@ -0,0 +1,45 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/subworkflows/yaml-schema.json +name: "stardist_resolift_morphology_ome_tif" +description: | + image-based segmentation approach with stardist to run import-segmentation in pixel coordinate space +keywords: + - stardist + - segmentation + - xeniumranger import-segmentation + - image-based segmentation +components: + - stardist + - resolift + - xeniumranger/import/segmentation + - convert/mask/uint32 + - extract/dapi +input: + - ch_morphology_image: + description: | + path to the morphology.ome.tif file + Structure: [ val(meta), path("path-to-morphology.ome.tif") ] + - ch_bundle_path: + description: | + path to the xenium bundle + Structure: [ val(meta), path("path-to-xenium-bundle") ] + - sharpen_tiff: + description: | + sharpen the input morphology.ome.tif file before running stardist + Structure: [ val("true") ] + - stardist_nuclei_model: + description: | + the stardist model to use for segmentation + Structure: [ val("2D_versatile_fluo") ] +output: + - coordinate_space: + description: | + the coordinate space in which xeniumranger import-segmentation was run + Structure: [ val("pixels") ] + - redefined_bundle: + description: | + the redefined xenium bundle generated with the segmentation results from stardist + Structure: [ val(meta), ["redefined-xenium-bundle"] ] +authors: + - "@khersameesh24" +maintainers: + - "@khersameesh24" diff --git a/subworkflows/local/utils_nfcore_spatialaxe_pipeline/main.nf b/subworkflows/local/utils_nfcore_spatialaxe_pipeline/main.nf index fda9b46e..d9cb4a0f 100644 --- a/subworkflows/local/utils_nfcore_spatialaxe_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_spatialaxe_pipeline/main.nf @@ -277,14 +277,14 @@ def validateInputParameters( // check if segmentation mask is provided in image mode and baysor method if (mode == 'image' && method == 'baysor') { if (!segmentation_mask) { - log.warn("⚠️ Missing segmentation mask with `--segmentation_mask` when pipeline is run in ${mode} and with the ${method}. Running in coordinate mode.") + error("❌ Error: Missing segmentation mask with `--segmentation_mask` option, when pipeline is run in ${mode} mode and with the ${method} method a prior segmentation mask should be provided.") } } // check if required arguments are provided for off-target probe tracking if (!mode && offtarget_probe_tracking) { if(!probes_fasta || !reference_annotations || !gene_synonyms) { - error("❌ Error: Missing required param(s) for off-target-proebe detection.") + error("❌ Error: Missing required param(s) for off-target-proebe detection. Please provide --probes_fasta, --reference_annotations, and --gene_synonyms.") } error("❌ Error: Use --mode qc and --offtraget_probe_tracking to run off-target probe tracking.") } diff --git a/subworkflows/local/opt_flip_track_stat/main.nf b/subworkflows/nf-core/opt_flip_track_stat/main.nf similarity index 72% rename from subworkflows/local/opt_flip_track_stat/main.nf rename to subworkflows/nf-core/opt_flip_track_stat/main.nf index 839768d2..63751fa4 100644 --- a/subworkflows/local/opt_flip_track_stat/main.nf +++ b/subworkflows/nf-core/opt_flip_track_stat/main.nf @@ -11,24 +11,19 @@ workflow OPT_FLIP_TRACK_STAT { main: - ch_versions = channel.empty() ch_summary = channel.empty() // correct probes that are aligning to opposite strand with `flip` OPT_FLIP(ch_probe_fasta, ch_references) - ch_versions = ch_versions.mix(OPT_FLIP.out.versions) // align query probe sequences to target transcriptome OPT_TRACK(OPT_FLIP.out.fwd_oriented_fa, ch_references) - ch_versions = ch_versions.mix(OPT_TRACK.out.versions) // summarizes opt binding predictions OPT_STAT(OPT_TRACK.out.probes2target, OPT_FLIP.out.fwd_oriented_fa, ch_gene_synonyms) - ch_versions = ch_versions.mix(OPT_STAT.out.versions) ch_summary = OPT_STAT.out.summary emit: - summary = ch_summary // channel: [ val(meta), ["collapsed_summary.tsv", "other-summary-files"]] - versions = ch_versions // channel: [ versions.yml ] -} + summary = ch_summary // channel: [ val(meta), ["collapsed_summary.tsv"]] +} \ No newline at end of file diff --git a/subworkflows/nf-core/opt_flip_track_stat/meta.yml b/subworkflows/nf-core/opt_flip_track_stat/meta.yml new file mode 100644 index 00000000..f52f69a3 --- /dev/null +++ b/subworkflows/nf-core/opt_flip_track_stat/meta.yml @@ -0,0 +1,45 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/subworkflows/yaml-schema.json +name: "OPT_FLIP_TRACK_STAT" +description: Off-target probe detection +keywords: + - opt + - flip + - track + - stat + - off-target-probe-detection + - probes + - off-target probes +components: + - opt/flip + - opt/track + - opt/stat +input: + - ch_probe_fasta: + type: file + description: | + The input channel containing the probes used in spatial experiment + Structure: [ val(meta), path('probes.fasta') ] + pattern: "*.{fasta,fa}" + - ch_references: + type: file + description: | + The input channel containing the reference fasta and reference gtf/gff files + Structure: [ val(meta), ["reference_annotations.{gff/gtf}"], ["reference_annotations.fa"]] + pattern: "*{.gff,.gtf},*{.fa,.fasta}" + - ch_gene_synonyms: + type: file + description: | + The input channel containing the gene synonyms file + Structure: [ path('gene_synonyms.csv') ] + pattern: "*.csv" +output: + - summary: + type: file + description: | + Channel containing the collapsed summary of the off-target probes + Structure: [ val(meta), path(collapsed_summary.tsv) ] + pattern: "*.tsv" +authors: + - "@khersameesh24" +maintainers: + - "@khersameesh24" diff --git a/subworkflows/nf-core/opt_flip_track_stat/tests/main.nf.test b/subworkflows/nf-core/opt_flip_track_stat/tests/main.nf.test new file mode 100644 index 00000000..ec7721e3 --- /dev/null +++ b/subworkflows/nf-core/opt_flip_track_stat/tests/main.nf.test @@ -0,0 +1,43 @@ +nextflow_workflow { + + name "Test OPT_FLIP_TRACK_STAT" + script "../main.nf" + workflow "OPT_FLIP_TRACK_STAT" + tag "subworkflows" + tag "subworkflows_nfcore" + tag "opt" + tag "opt/flip" + tag "opt/stat" + tag "opt/track" + tag "subworkflows/opt_flip_track_stat" + + test("off-target probe tracker - opt") { + + when { + workflow { + """ + input[0] = Channel.of( + [ + [ id: 'test_probes' ], + file(params.modules_testdata_base_path + 'spatial_omics/probe_detection/probes.fa', checkIfExists: true) + ] + ) + input[1] = [ + [ id:'test_references' ], + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/minigenome.gtf', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/minigenome.fa', checkIfExists: true) + ] + input[2] = [file(params.modules_testdata_base_path + 'spatial_omics/probe_detection/gene_synonyms.csv', checkIfExists: true)] + """ + } + } + + then { + assertAll( + { assert workflow.success }, + { assert snapshot(sanitizeOutput(workflow.out)).match() }, + ) + } + } + +} diff --git a/subworkflows/nf-core/opt_flip_track_stat/tests/main.nf.test.snap b/subworkflows/nf-core/opt_flip_track_stat/tests/main.nf.test.snap new file mode 100644 index 00000000..93a9548a --- /dev/null +++ b/subworkflows/nf-core/opt_flip_track_stat/tests/main.nf.test.snap @@ -0,0 +1,21 @@ +{ + "off-target probe tracker - opt": { + "content": [ + { + "summary": [ + [ + { + "id": "test_probes" + }, + "collapsed_summary.tsv:md5,b2884d9c8c89124d3cbbbc1223d81c99" + ] + ] + } + ], + "timestamp": "2026-06-02T20:54:38.545399031", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.3" + } + } +} \ No newline at end of file diff --git a/tests/coordinate_mode.nf.test.snap b/tests/coordinate_mode.nf.test.snap index 72a452ec..56baade9 100644 --- a/tests/coordinate_mode.nf.test.snap +++ b/tests/coordinate_mode.nf.test.snap @@ -3,10 +3,10 @@ "content": [ { "PROSEG2BAYSOR": { - "proseg": "3.1.0" + "proseg": "3.1.1" }, "PROSEG": { - "proseg": "3.1.0" + "proseg": "3.1.1" }, "SPATIALDATA_MERGE_RAW_REDEFINED": { "spatialdata": "0.7.2" @@ -46,15 +46,17 @@ "coordinate/multiqc/redefined_bundle/multiqc_plots/.stub", "coordinate/multiqc/redefined_bundle/multiqc_report.html", "coordinate/proseg", - "coordinate/proseg/preset", - "coordinate/proseg/preset/test_run", - "coordinate/proseg/preset/test_run/cell-polygons.geojson.gz", - "coordinate/proseg/preset/test_run/proseg-output.zarr", - "coordinate/proseg/preset/test_run/transcript-metadata.csv.gz", + "coordinate/proseg/proseg", + "coordinate/proseg/proseg/test_run.zarr", + "coordinate/proseg/proseg/test_run_cell-metadata.csv.gz", + "coordinate/proseg/proseg/test_run_cell-polygons-layers.geojson.gz", + "coordinate/proseg/proseg/test_run_cell-polygons-union.geojson.gz", + "coordinate/proseg/proseg/test_run_cell-polygons.geojson.gz", + "coordinate/proseg/proseg/test_run_expected-counts.csv.gz", + "coordinate/proseg/proseg/test_run_transcript-metadata.csv.gz", "coordinate/proseg/proseg2baysor", - "coordinate/proseg/proseg2baysor/test_run", - "coordinate/proseg/proseg2baysor/test_run/cell-polygons.geojson", - "coordinate/proseg/proseg2baysor/test_run/transcript-metadata.csv", + "coordinate/proseg/proseg2baysor/test_run_cell-polygons.geojson", + "coordinate/proseg/proseg2baysor/test_run_transcript-metadata.csv", "coordinate/spatialdata", "coordinate/spatialdata/merge", "coordinate/spatialdata/merge/spatialdata", @@ -99,9 +101,9 @@ "coordinate/untar/test_run/transcripts.parquet", "coordinate/untar/test_run/transcripts.zarr.zip", "coordinate/xeniumranger", - "coordinate/xeniumranger/import_segementation", - "coordinate/xeniumranger/import_segementation/test_run", - "coordinate/xeniumranger/import_segementation/test_run/experiment.xenium", + "coordinate/xeniumranger/import_segmentation", + "coordinate/xeniumranger/import_segmentation/test_run", + "coordinate/xeniumranger/import_segmentation/test_run/experiment.xenium", "pipeline_info", "pipeline_info/nf_core_spatialaxe_software_mqc_versions.yml" ], @@ -112,8 +114,15 @@ ".stub:md5,d41d8cd98f00b204e9800998ecf8427e", ".stub:md5,d41d8cd98f00b204e9800998ecf8427e", "multiqc_report.html:md5,d41d8cd98f00b204e9800998ecf8427e", - "cell-polygons.geojson:md5,d41d8cd98f00b204e9800998ecf8427e", - "transcript-metadata.csv:md5,d41d8cd98f00b204e9800998ecf8427e", + "test_run.zarr:md5,d41d8cd98f00b204e9800998ecf8427e", + "test_run_cell-metadata.csv.gz:md5,68b329da9893e34099c7d8ad5cb9c940", + "test_run_cell-polygons-layers.geojson.gz:md5,68b329da9893e34099c7d8ad5cb9c940", + "test_run_cell-polygons-union.geojson.gz:md5,68b329da9893e34099c7d8ad5cb9c940", + "test_run_cell-polygons.geojson.gz:md5,68b329da9893e34099c7d8ad5cb9c940", + "test_run_expected-counts.csv.gz:md5,68b329da9893e34099c7d8ad5cb9c940", + "test_run_transcript-metadata.csv.gz:md5,68b329da9893e34099c7d8ad5cb9c940", + "test_run_cell-polygons.geojson:md5,d41d8cd98f00b204e9800998ecf8427e", + "test_run_transcript-metadata.csv:md5,d41d8cd98f00b204e9800998ecf8427e", "fake_file.txt:md5,d41d8cd98f00b204e9800998ecf8427e", "fake_file.txt:md5,d41d8cd98f00b204e9800998ecf8427e", "fake_file.txt:md5,d41d8cd98f00b204e9800998ecf8427e", @@ -143,10 +152,10 @@ "experiment.xenium:md5,d41d8cd98f00b204e9800998ecf8427e" ] ], - "timestamp": "2026-05-18T21:58:08.945192717", + "timestamp": "2026-06-27T17:41:01.307236782", "meta": { "nf-test": "0.9.5", - "nextflow": "25.10.4" + "nextflow": "26.04.4" } } } \ No newline at end of file diff --git a/tests/default.nf.test.snap b/tests/default.nf.test.snap index 3289c2a0..0ba4b83a 100644 --- a/tests/default.nf.test.snap +++ b/tests/default.nf.test.snap @@ -3,10 +3,10 @@ "content": [ { "PROSEG2BAYSOR": { - "proseg": "3.1.0" + "proseg": "3.1.1" }, "PROSEG": { - "proseg": "3.1.0" + "proseg": "3.1.1" }, "SPATIALDATA_MERGE_RAW_REDEFINED": { "spatialdata": "0.7.2" @@ -46,15 +46,17 @@ "coordinate/multiqc/redefined_bundle/multiqc_plots/.stub", "coordinate/multiqc/redefined_bundle/multiqc_report.html", "coordinate/proseg", - "coordinate/proseg/preset", - "coordinate/proseg/preset/test_run", - "coordinate/proseg/preset/test_run/cell-polygons.geojson.gz", - "coordinate/proseg/preset/test_run/proseg-output.zarr", - "coordinate/proseg/preset/test_run/transcript-metadata.csv.gz", + "coordinate/proseg/proseg", + "coordinate/proseg/proseg/test_run.zarr", + "coordinate/proseg/proseg/test_run_cell-metadata.csv.gz", + "coordinate/proseg/proseg/test_run_cell-polygons-layers.geojson.gz", + "coordinate/proseg/proseg/test_run_cell-polygons-union.geojson.gz", + "coordinate/proseg/proseg/test_run_cell-polygons.geojson.gz", + "coordinate/proseg/proseg/test_run_expected-counts.csv.gz", + "coordinate/proseg/proseg/test_run_transcript-metadata.csv.gz", "coordinate/proseg/proseg2baysor", - "coordinate/proseg/proseg2baysor/test_run", - "coordinate/proseg/proseg2baysor/test_run/cell-polygons.geojson", - "coordinate/proseg/proseg2baysor/test_run/transcript-metadata.csv", + "coordinate/proseg/proseg2baysor/test_run_cell-polygons.geojson", + "coordinate/proseg/proseg2baysor/test_run_transcript-metadata.csv", "coordinate/spatialdata", "coordinate/spatialdata/merge", "coordinate/spatialdata/merge/spatialdata", @@ -99,9 +101,9 @@ "coordinate/untar/test_run/transcripts.parquet", "coordinate/untar/test_run/transcripts.zarr.zip", "coordinate/xeniumranger", - "coordinate/xeniumranger/import_segementation", - "coordinate/xeniumranger/import_segementation/test_run", - "coordinate/xeniumranger/import_segementation/test_run/experiment.xenium", + "coordinate/xeniumranger/import_segmentation", + "coordinate/xeniumranger/import_segmentation/test_run", + "coordinate/xeniumranger/import_segmentation/test_run/experiment.xenium", "pipeline_info", "pipeline_info/nf_core_spatialaxe_software_mqc_versions.yml" ], @@ -112,8 +114,15 @@ ".stub:md5,d41d8cd98f00b204e9800998ecf8427e", ".stub:md5,d41d8cd98f00b204e9800998ecf8427e", "multiqc_report.html:md5,d41d8cd98f00b204e9800998ecf8427e", - "cell-polygons.geojson:md5,d41d8cd98f00b204e9800998ecf8427e", - "transcript-metadata.csv:md5,d41d8cd98f00b204e9800998ecf8427e", + "test_run.zarr:md5,d41d8cd98f00b204e9800998ecf8427e", + "test_run_cell-metadata.csv.gz:md5,68b329da9893e34099c7d8ad5cb9c940", + "test_run_cell-polygons-layers.geojson.gz:md5,68b329da9893e34099c7d8ad5cb9c940", + "test_run_cell-polygons-union.geojson.gz:md5,68b329da9893e34099c7d8ad5cb9c940", + "test_run_cell-polygons.geojson.gz:md5,68b329da9893e34099c7d8ad5cb9c940", + "test_run_expected-counts.csv.gz:md5,68b329da9893e34099c7d8ad5cb9c940", + "test_run_transcript-metadata.csv.gz:md5,68b329da9893e34099c7d8ad5cb9c940", + "test_run_cell-polygons.geojson:md5,d41d8cd98f00b204e9800998ecf8427e", + "test_run_transcript-metadata.csv:md5,d41d8cd98f00b204e9800998ecf8427e", "fake_file.txt:md5,d41d8cd98f00b204e9800998ecf8427e", "fake_file.txt:md5,d41d8cd98f00b204e9800998ecf8427e", "fake_file.txt:md5,d41d8cd98f00b204e9800998ecf8427e", @@ -143,10 +152,10 @@ "experiment.xenium:md5,d41d8cd98f00b204e9800998ecf8427e" ] ], - "timestamp": "2026-05-18T21:55:37.544217665", + "timestamp": "2026-06-27T17:41:35.331319572", "meta": { "nf-test": "0.9.5", - "nextflow": "25.10.4" + "nextflow": "26.04.4" } } } \ No newline at end of file diff --git a/tests/image_mode.nf.test.snap b/tests/image_mode.nf.test.snap index a9b73c2c..be7cc346 100644 --- a/tests/image_mode.nf.test.snap +++ b/tests/image_mode.nf.test.snap @@ -39,18 +39,17 @@ [ "coordinate", "coordinate/xeniumranger", - "coordinate/xeniumranger/import_segementation", - "coordinate/xeniumranger/import_segementation/test_run", - "coordinate/xeniumranger/import_segementation/test_run/experiment.xenium", + "coordinate/xeniumranger/import_segmentation", + "coordinate/xeniumranger/import_segmentation/test_run", + "coordinate/xeniumranger/import_segmentation/test_run/experiment.xenium", "image", "image/baysor", "image/baysor/preprocess", "image/baysor/preprocess/test_run", "image/baysor/preprocess/test_run/filtered_transcripts.csv", "image/baysor/run", - "image/baysor/run/test_run", - "image/baysor/run/test_run/segmentation.csv", - "image/baysor/run/test_run/segmentation_polygons_2d.json", + "image/baysor/run/test_run_segmentation.csv", + "image/baysor/run/test_run_segmentation_polygons_2d.json", "image/cellpose_cells", "image/cellpose_cells/test_run", "image/cellpose_cells/test_run/morphology_focus_0000.ome_cp_masks.tif", @@ -120,8 +119,8 @@ [ "experiment.xenium:md5,d41d8cd98f00b204e9800998ecf8427e", "filtered_transcripts.csv:md5,d41d8cd98f00b204e9800998ecf8427e", - "segmentation.csv:md5,d41d8cd98f00b204e9800998ecf8427e", - "segmentation_polygons_2d.json:md5,d41d8cd98f00b204e9800998ecf8427e", + "test_run_segmentation.csv:md5,d41d8cd98f00b204e9800998ecf8427e", + "test_run_segmentation_polygons_2d.json:md5,d41d8cd98f00b204e9800998ecf8427e", "morphology_focus_0000.ome_cp_masks.tif:md5,d41d8cd98f00b204e9800998ecf8427e", ".stub:md5,d41d8cd98f00b204e9800998ecf8427e", ".stub:md5,d41d8cd98f00b204e9800998ecf8427e", @@ -158,10 +157,10 @@ "resized_morphology_focus_0000.ome_cp_masks.tif.tif:md5,d41d8cd98f00b204e9800998ecf8427e" ] ], - "timestamp": "2026-05-18T22:00:39.030306506", + "timestamp": "2026-06-27T17:42:50.445458354", "meta": { "nf-test": "0.9.5", - "nextflow": "25.10.4" + "nextflow": "26.04.4" } } } \ No newline at end of file diff --git a/tests/preview_mode.nf.test.snap b/tests/preview_mode.nf.test.snap index 3d364f04..39e815ea 100644 --- a/tests/preview_mode.nf.test.snap +++ b/tests/preview_mode.nf.test.snap @@ -11,7 +11,7 @@ "EXTRACT_PREVIEW_DATA": { "python": "3.14.4" }, - "PARQUET_TO_CSV": { + "PARQUET2CSV": { "pyarrow": "24.0.0" }, "UNTAR": { @@ -30,8 +30,7 @@ "preview/baysor/create_dataset/test_run", "preview/baysor/create_dataset/test_run/sampled_transcripts.csv", "preview/baysor/preview", - "preview/baysor/preview/test_run", - "preview/baysor/preview/test_run/preview.html", + "preview/baysor/preview/test_run_preview.html", "preview/multiqc", "preview/multiqc/multiqc_data", "preview/multiqc/multiqc_data/.stub", @@ -64,9 +63,9 @@ "preview/untar/test_run/transcripts.parquet", "preview/untar/test_run/transcripts.zarr.zip", "preview/utility", - "preview/utility/parquet_to_csv", - "preview/utility/parquet_to_csv/test_run", - "preview/utility/parquet_to_csv/test_run/transcripts.parquet.csv", + "preview/utility/parquet2csv", + "preview/utility/parquet2csv/test_run", + "preview/utility/parquet2csv/test_run/transcripts.parquet.csv", "preview/utility/preview_data", "preview/utility/preview_data/test_run", "preview/utility/preview_data/test_run/gene_structure_mqc.tsv", @@ -77,7 +76,7 @@ ], [ "sampled_transcripts.csv:md5,d41d8cd98f00b204e9800998ecf8427e", - "preview.html:md5,d41d8cd98f00b204e9800998ecf8427e", + "test_run_preview.html:md5,d41d8cd98f00b204e9800998ecf8427e", ".stub:md5,d41d8cd98f00b204e9800998ecf8427e", ".stub:md5,d41d8cd98f00b204e9800998ecf8427e", "multiqc_report.html:md5,d41d8cd98f00b204e9800998ecf8427e", @@ -111,10 +110,10 @@ "umap_mqc.tsv:md5,d41d8cd98f00b204e9800998ecf8427e" ] ], - "timestamp": "2026-06-17T11:15:30.764424637", + "timestamp": "2026-06-27T17:43:45.016177869", "meta": { "nf-test": "0.9.5", - "nextflow": "26.04.2" + "nextflow": "26.04.4" } } } \ No newline at end of file diff --git a/tests/segfree_mode.nf.test.snap b/tests/segfree_mode.nf.test.snap index ac663a43..62a7a6c3 100644 --- a/tests/segfree_mode.nf.test.snap +++ b/tests/segfree_mode.nf.test.snap @@ -24,8 +24,7 @@ "segfree/baysor/preprocess/test_run", "segfree/baysor/preprocess/test_run/filtered_transcripts.csv", "segfree/baysor/segfree", - "segfree/baysor/segfree/test_run", - "segfree/baysor/segfree/test_run/ncvs.loom", + "segfree/baysor/segfree/test_run_ncvs.loom", "segfree/multiqc", "segfree/multiqc/multiqc_data", "segfree/multiqc/multiqc_data/.stub", @@ -60,7 +59,7 @@ ], [ "filtered_transcripts.csv:md5,d41d8cd98f00b204e9800998ecf8427e", - "ncvs.loom:md5,d41d8cd98f00b204e9800998ecf8427e", + "test_run_ncvs.loom:md5,d41d8cd98f00b204e9800998ecf8427e", ".stub:md5,d41d8cd98f00b204e9800998ecf8427e", ".stub:md5,d41d8cd98f00b204e9800998ecf8427e", "multiqc_report.html:md5,d41d8cd98f00b204e9800998ecf8427e", @@ -88,10 +87,10 @@ "transcripts.zarr.zip:md5,d41d8cd98f00b204e9800998ecf8427e" ] ], - "timestamp": "2026-05-18T22:02:39.947804998", + "timestamp": "2026-06-27T17:44:23.349495745", "meta": { "nf-test": "0.9.5", - "nextflow": "25.10.4" + "nextflow": "26.04.4" } } } \ No newline at end of file diff --git a/tests/test_xenium_patch/test_stitch_transcripts.py b/tests/test_xenium_patch/test_stitch_transcripts.py index 5419c897..b7dd440d 100644 --- a/tests/test_xenium_patch/test_stitch_transcripts.py +++ b/tests/test_xenium_patch/test_stitch_transcripts.py @@ -2,6 +2,7 @@ import importlib.util import json +import subprocess import sys from pathlib import Path @@ -14,16 +15,15 @@ # Import the standalone script from module resources # --------------------------------------------------------------------------- -_SCRIPT = ( - Path(__file__).resolve().parents[2] - / "modules/local/xenium_patch/stitch/resources/usr/bin/stitch_transcripts.py" +_SCRIPT = Path(__file__).resolve().parents[2] / "bin/xenium_patch_stitch_transcripts.py" +_spec = importlib.util.spec_from_file_location( + "xenium_patch_stitch_transcripts", _SCRIPT ) -_spec = importlib.util.spec_from_file_location("stitch_transcripts", _SCRIPT) _mod = importlib.util.module_from_spec(_spec) -sys.modules["stitch_transcripts"] = _mod +sys.modules["xenium_patch_stitch_transcripts"] = _mod _spec.loader.exec_module(_mod) -from stitch_transcripts import ( # noqa: E402 +from xenium_patch_stitch_transcripts import ( # noqa: E402 Bounds, PatchGridMetadata, PatchInfo, @@ -863,3 +863,141 @@ def test_baysor_two_patches_empty_cell(self, tmp_path: Path): with open(geo_out) as f: geo = json.load(f) assert len(geo["features"]) == 2 + + +# --------------------------------------------------------------------------- +# min-transcripts-per-cell filter — regression for the dropped CLI argument +# (XENIUM_PATCH_STITCH failed with "unrecognized arguments: +# --min-transcripts-per-cell 50" after the script was split; see +# docs/failures/2026-06-27_xenium-patch-stitch-min-transcripts-arg.md) +# --------------------------------------------------------------------------- + + +def _write_one_patch_two_cells(tmp_path: Path) -> Path: + """One full-extent patch: cell_big (3 transcripts) + cell_small (1).""" + p0 = _make_patch_info( + "patch_0", + 0, + 0, + global_x=(0.0, 1000.0), + global_y=(0.0, 1000.0), + core_x=(0.0, 1000.0), + core_y=(0.0, 1000.0), + ) + metadata = _make_metadata([p0]) + patches_dir = tmp_path / "patches" + _write_grid_json(metadata, patches_dir / "patch_grid.json") + _write_patch_csv( + patches_dir / "patch_0", + [ + { + "transcript_id": "tx_1", + "x": "100.0", + "y": "100.0", + "gene": "A", + "cell": "cell_big", + "is_noise": "0", + }, + { + "transcript_id": "tx_2", + "x": "110.0", + "y": "110.0", + "gene": "B", + "cell": "cell_big", + "is_noise": "0", + }, + { + "transcript_id": "tx_3", + "x": "120.0", + "y": "120.0", + "gene": "C", + "cell": "cell_big", + "is_noise": "0", + }, + { + "transcript_id": "tx_small", + "x": "800.0", + "y": "800.0", + "gene": "D", + "cell": "cell_small", + "is_noise": "0", + }, + ], + ) + _write_patch_geojson( + patches_dir / "patch_0", + { + "cell_big": Polygon([(50, 50), (200, 50), (200, 200), (50, 200)]), + "cell_small": Polygon([(750, 750), (850, 750), (850, 850), (750, 850)]), + }, + ) + return patches_dir + + +class TestMinTranscriptsFilter: + def test_filter_drops_small_cells(self, tmp_path: Path): + """min_transcripts_per_cell=2 reassigns the 1-transcript cell to noise + and removes its polygon, keeping the 3-transcript cell.""" + patches_dir = _write_one_patch_two_cells(tmp_path) + output_dir = tmp_path / "output" + stitch_transcript_assignments( + patches_dir=patches_dir, + output_dir=output_dir, + max_workers=1, + min_transcripts_per_cell=2, + ) + + merged = pa_csv.read_csv(output_dir / "xr-transcript-metadata.csv") + tid = merged.column("transcript_id").to_pylist() + cell = merged.column("cell").to_pylist() + + # The lone transcript of the small cell is demoted to noise (empty cell) + assert cell[tid.index("tx_small")] == "", ( + "small cell's transcript should be reassigned to noise" + ) + # The 3-transcript cell survives + assert cell[tid.index("tx_1")] != "", "big cell should be retained" + + # Its polygon is dropped — only the surviving cell remains + with open(output_dir / "xr-cell-polygons.geojson") as f: + geo = json.load(f) + assert len(geo["features"]) == 1 + + def test_no_filter_keeps_all_cells(self, tmp_path: Path): + """min_transcripts_per_cell=0 (default) keeps both cells — control.""" + patches_dir = _write_one_patch_two_cells(tmp_path) + output_dir = tmp_path / "output" + stitch_transcript_assignments( + patches_dir=patches_dir, + output_dir=output_dir, + max_workers=1, + min_transcripts_per_cell=0, + ) + with open(output_dir / "xr-cell-polygons.geojson") as f: + geo = json.load(f) + assert len(geo["features"]) == 2 + + def test_cli_accepts_min_transcripts_per_cell(self, tmp_path: Path): + """Reproduces the production failure: invoke the script exactly as the + module does, including --min-transcripts-per-cell, via the real CLI. + The argparse contract must accept the flag (no 'unrecognized arguments').""" + patches_dir = _write_one_patch_two_cells(tmp_path) + output_dir = tmp_path / "output" + result = subprocess.run( + [ + sys.executable, + str(_SCRIPT), + "--patches", + str(patches_dir), + "--output", + str(output_dir), + "--min-transcripts-per-cell", + "50", + ], + capture_output=True, + text=True, + ) + assert "unrecognized arguments" not in result.stderr, result.stderr + assert result.returncode == 0, ( + f"script exited {result.returncode}\nstderr:\n{result.stderr}" + ) diff --git a/workflows/spatialaxe.nf b/workflows/spatialaxe.nf index 8e11f058..8ff78dfb 100644 --- a/workflows/spatialaxe.nf +++ b/workflows/spatialaxe.nf @@ -44,7 +44,7 @@ include { XENIUMRANGER_IMPORT_SEGMENTATION_REDEFINE_BUNDLE } from '../subworkflo include { SPATIALDATA_WRITE_META_MERGE } from '../subworkflows/local/spatialdata_write_meta_merge/main' // qc layer subworkflows -include { OPT_FLIP_TRACK_STAT } from '../subworkflows/local/opt_flip_track_stat/main' +include { OPT_FLIP_TRACK_STAT } from '../subworkflows/nf-core/opt_flip_track_stat/main' /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -58,9 +58,11 @@ workflow SPATIALAXE { alignment_csv baysor_config baysor_prior - baysor_scale + // baysor_scale baysor_tiling - baysor_tiling_scale + // baysor_tiling_scale + baysor_prior_confidence + min_transcripts_per_cell buffer_samples buffer_size cell_segmentation_only @@ -105,26 +107,26 @@ workflow SPATIALAXE { ch_versions = channel.empty() - ch_input = channel.empty() - ch_config = channel.empty() - ch_features = channel.value([]) - ch_raw_bundle = channel.empty() - ch_gene_panel = channel.empty() - ch_qc_reports = channel.empty() - ch_bundle_path = channel.empty() - ch_preview_html = channel.empty() - ch_exp_metadata = channel.empty() - ch_gene_synonyms = channel.empty() - ch_multiqc_files = channel.empty() - ch_multiqc_report = channel.empty() - ch_qupath_polygons = channel.empty() - ch_morphology_image = channel.empty() - ch_redefined_bundle = channel.empty() - ch_coordinate_space = channel.empty() - ch_panel_probes_fasta = channel.empty() - ch_transcripts_file = channel.empty() - ch_reference_annotations = channel.empty() - ch_multiqc_pre_xr_report = channel.empty() + ch_input = channel.empty() + ch_features = channel.value([]) + ch_raw_bundle = channel.empty() + ch_gene_panel = channel.empty() + ch_qc_reports = channel.empty() + ch_bundle_path = channel.empty() + ch_preview_html = channel.empty() + ch_exp_metadata = channel.empty() + ch_gene_synonyms = channel.empty() + ch_baysor_config = channel.empty() + ch_multiqc_files = channel.empty() + ch_multiqc_report = channel.empty() + ch_qupath_polygons = channel.empty() + ch_morphology_image = channel.empty() + ch_redefined_bundle = channel.empty() + ch_coordinate_space = channel.empty() + ch_panel_probes_fasta = channel.empty() + ch_transcripts_parquet = channel.empty() + ch_reference_annotations = channel.empty() + ch_multiqc_pre_xr_report = channel.empty() ch_multiqc_post_xr_report = channel.empty() @@ -220,7 +222,7 @@ workflow SPATIALAXE { } // get transcript.parquet from the xenium bundle - ch_transcripts_file = ch_input.map { meta, bundle, _image -> + ch_transcripts_parquet = ch_input.map { meta, bundle, _image -> def transcripts_parquet = file( bundle.toString().replaceFirst(/\/$/, '') + "/transcripts.parquet", checkIfExists: true @@ -266,11 +268,20 @@ workflow SPATIALAXE { } // get baysor xenium config - ch_config = channel.fromPath( - "${projectDir}/assets/config/xenium.toml", - checkIfExists: true - ) - .flatten() + if (baysor_config) { + ch_baysor_config = channel.fromPath( + baysor_config, + checkIfExists: true + ) + .flatten() + } + else { + ch_baysor_config = channel.fromPath( + "${projectDir}/assets/config/xenium.toml", + checkIfExists: true + ) + .flatten() + } // get segmentation mask if provided with --segmentation_mask for the baysor method if (segmentation_mask) { @@ -380,8 +391,8 @@ workflow SPATIALAXE { if (mode == 'preview') { BAYSOR_GENERATE_PREVIEW( - ch_transcripts_file, - ch_config, + ch_transcripts_parquet, + ch_baysor_config, ) ch_preview_html = BAYSOR_GENERATE_PREVIEW.out.preview_html } @@ -416,12 +427,13 @@ workflow SPATIALAXE { // trigger the default image-based workflow if no method is specified if (!method) { + prior_column = baysor_prior == 'cells' ? 'cell_id' : null CELLPOSE_BAYSOR_IMPORT_SEGMENTATION( ch_morphology_image, ch_bundle_path, - ch_transcripts_file, + ch_transcripts_parquet, ch_exp_metadata, - ch_config, + ch_baysor_config, cell_segmentation_only, cellpose_model, max_x, @@ -429,6 +441,8 @@ workflow SPATIALAXE { min_qv, min_x, min_y, + prior_column, + min_transcripts_per_cell, nucleus_segmentation_only, sharpen_tiff, stardist_nuclei_model, @@ -452,20 +466,26 @@ workflow SPATIALAXE { if (method == 'baysor') { if (segmentation_mask) { + + prior_column = baysor_prior == 'cells' ? 'cell_id' : null + prior_confidence = baysor_prior != null ? baysor_prior_confidence : null BAYSOR_RUN_PRIOR_SEGMENTATION_MASK( ch_bundle_path, - ch_transcripts_file, + ch_transcripts_parquet, ch_segmentation_mask, - ch_config, + ch_baysor_config, max_x, max_y, min_qv, min_x, min_y, + prior_column, + prior_confidence, + min_transcripts_per_cell ) + ch_redefined_bundle = BAYSOR_RUN_PRIOR_SEGMENTATION_MASK.out.redefined_bundle + ch_coordinate_space = BAYSOR_RUN_PRIOR_SEGMENTATION_MASK.out.coordinate_space } - ch_redefined_bundle = BAYSOR_RUN_PRIOR_SEGMENTATION_MASK.out.redefined_bundle - ch_coordinate_space = BAYSOR_RUN_PRIOR_SEGMENTATION_MASK.out.coordinate_space } // run cellpose on the morphology_ome.tif @@ -511,14 +531,14 @@ workflow SPATIALAXE { if (tiling) { PROSEG_PRESET_PROSEG2BAYSOR_TILED( ch_bundle_path, - ch_transcripts_file, + ch_transcripts_parquet, ) ch_redefined_bundle = PROSEG_PRESET_PROSEG2BAYSOR_TILED.out.redefined_bundle ch_coordinate_space = PROSEG_PRESET_PROSEG2BAYSOR_TILED.out.coordinate_space } else { PROSEG_PRESET_PROSEG2BAYSOR( ch_bundle_path, - ch_transcripts_file, + ch_transcripts_parquet, ) ch_redefined_bundle = PROSEG_PRESET_PROSEG2BAYSOR.out.redefined_bundle ch_coordinate_space = PROSEG_PRESET_PROSEG2BAYSOR.out.coordinate_space @@ -530,7 +550,7 @@ workflow SPATIALAXE { SEGGER_CREATE_TRAIN_PREDICT( ch_bundle_path, - ch_transcripts_file, + ch_transcripts_parquet, segger_model, ) ch_redefined_bundle = SEGGER_CREATE_TRAIN_PREDICT.out.redefined_bundle @@ -550,19 +570,18 @@ workflow SPATIALAXE { BAYSOR_RUN_TRANSCRIPTS_PARQUET( ch_bundle_path, - ch_transcripts_file, + ch_transcripts_parquet, ch_morphology_image, - ch_config, + ch_baysor_config, ch_prior_mask, - baysor_config, - baysor_scale, baysor_tiling, - baysor_tiling_scale, max_x, max_y, min_qv, min_x, min_y, + prior_column, + min_transcripts_per_cell ) ch_redefined_bundle = BAYSOR_RUN_TRANSCRIPTS_PARQUET.out.redefined_bundle ch_coordinate_space = BAYSOR_RUN_TRANSCRIPTS_PARQUET.out.coordinate_space @@ -622,13 +641,13 @@ workflow SPATIALAXE { if (!method || method == 'baysor') { BAYSOR_GENERATE_SEGFREE( - ch_transcripts_file, - ch_config, + ch_transcripts_parquet, + ch_baysor_config, max_x, max_y, min_qv, min_x, - min_y, + min_y ) } @@ -636,7 +655,7 @@ workflow SPATIALAXE { if (method == 'ficture') { FICTURE_PREPROCESS_MODEL( - ch_transcripts_file, + ch_transcripts_parquet, ch_features, features, )