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 51860d55..cc827ec2 100644
--- a/conf/modules.config
+++ b/conf/modules.config
@@ -79,7 +79,7 @@ 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,
]
}
@@ -88,12 +88,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 {
@@ -108,8 +105,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,
@@ -239,9 +234,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 15827672..9d8e8864 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,
@@ -82,6 +84,7 @@ workflow NFCORE_SPATIALAXE {
params.spoqc,
)
emit:
+
multiqc_report = SPATIALAXE.out.multiqc_report // channel: /path/to/multiqc_report.html
}
/*
diff --git a/modules.json b/modules.json
index 6bcc4bde..5f07cc4b 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": "1a545fcbd762911c21a64ced3dbef99b2b51ac75",
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 66d98351..4a42c18c 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 )
@@ -119,10 +60,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
@@ -162,6 +103,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
@@ -382,7 +331,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 = '!>=26.04.6'
diff --git a/nextflow_schema.json b/nextflow_schema.json
index 8ba05233..5ff73ac1 100644
--- a/nextflow_schema.json
+++ b/nextflow_schema.json
@@ -306,6 +306,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,
@@ -318,6 +323,7 @@
},
"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": {
@@ -327,6 +333,7 @@
},
"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": {
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 df9a6680..7221f295 100644
--- a/subworkflows/local/baysor_run_prior_segmentation_mask/main.nf
+++ b/subworkflows/local/baysor_run_prior_segmentation_mask/main.nf
@@ -6,8 +6,11 @@ include { BAYSOR_PREPROCESS_TRANSCRIPTS } from '../../../modules/local/baysor
include { BAYSOR_RUN } from '../../../modules/local/baysor/run/main'
include { XENIUMRANGER_IMPORTSEGMENTATION } from '../../../modules/nf-core/xeniumranger/importsegmentation/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"] ]
@@ -22,39 +25,53 @@ workflow BAYSOR_RUN_PRIOR_SEGMENTATION_MASK {
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
+
+
+ // 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
- ch_baysor_input = ch_transcripts
+ // 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
@@ -80,6 +97,7 @@ workflow BAYSOR_RUN_PRIOR_SEGMENTATION_MASK {
ch_redefined_bundle = XENIUMRANGER_IMPORTSEGMENTATION.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 eeaa6835..afe7e232 100644
--- a/subworkflows/local/baysor_run_transcripts_parquet/main.nf
+++ b/subworkflows/local/baysor_run_transcripts_parquet/main.nf
@@ -9,11 +9,13 @@
// 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_IMPORTSEGMENTATION } from '../../../modules/nf-core/xeniumranger/importsegmentation/main'
@@ -39,14 +41,32 @@ workflow BAYSOR_RUN_TRANSCRIPTS_PARQUET {
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
+ )
- // ── TILED PATH ──────────────────────────────────────────────────
+ // keep meta-keyed for non-tiled join
+ ch_scale_factor_by_meta = BAYSOR_ESTIMATE_SCALE_FACTOR.out.scale_factor
+
+ // 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 )
@@ -64,16 +84,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
@@ -126,7 +149,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,
@@ -135,17 +158,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
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 5e69d2d9..01a1094b 100644
--- a/subworkflows/local/baysor_run_transcripts_parquet_tiled/main.nf
+++ b/subworkflows/local/baysor_run_transcripts_parquet_tiled/main.nf
@@ -2,9 +2,11 @@
// 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_IMPORTSEGMENTATION } from '../../../modules/nf-core/xeniumranger/importsegmentation/main'
@@ -24,9 +26,10 @@ workflow BAYSOR_RUN_TRANSCRIPTS_PARQUET_TILED {
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
@@ -48,17 +51,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
@@ -100,6 +114,7 @@ workflow BAYSOR_RUN_TRANSCRIPTS_PARQUET_TILED {
XENIUMRANGER_IMPORTSEGMENTATION ( ch_xr )
emit:
+
coordinate_space = ch_coordinate_space // channel: [ "microns" ]
redefined_bundle = XENIUMRANGER_IMPORTSEGMENTATION.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 4f90ba49..d213699c 100644
--- a/subworkflows/local/cellpose_baysor_import_segmentation/main.nf
+++ b/subworkflows/local/cellpose_baysor_import_segmentation/main.nf
@@ -2,10 +2,8 @@
// 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'
@@ -13,11 +11,18 @@ include { RESIZE_TIF } from '../../../modules/local/utilit
include { GET_TRANSCRIPTS_COORDINATES } from '../../../modules/local/utility/get_coordinates/main'
include { XENIUMRANGER_IMPORTSEGMENTATION } from '../../../modules/nf-core/xeniumranger/importsegmentation/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 +32,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
@@ -37,6 +44,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
@@ -77,14 +87,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
@@ -108,16 +128,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) {
@@ -139,32 +160,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)
}
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 47b38013..c1c24117 100644
--- a/subworkflows/local/proseg_preset_proseg2baysor/main.nf
+++ b/subworkflows/local/proseg_preset_proseg2baysor/main.nf
@@ -15,10 +15,11 @@ workflow PROSEG_PRESET_PROSEG2BAYSOR {
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)
@@ -26,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,
diff --git a/subworkflows/local/proseg_preset_proseg2baysor_tiled/main.nf b/subworkflows/local/proseg_preset_proseg2baysor_tiled/main.nf
index a39242c5..bfd5318e 100644
--- a/subworkflows/local/proseg_preset_proseg2baysor_tiled/main.nf
+++ b/subworkflows/local/proseg_preset_proseg2baysor_tiled/main.nf
@@ -8,6 +8,9 @@ include { PROSEG2BAYSOR } from '../../../modules/local/proseg
include { XENIUM_PATCH_STITCH } from '../../../modules/local/xenium_patch/stitch/main'
include { XENIUMRANGER_IMPORTSEGMENTATION } from '../../../modules/nf-core/xeniumranger/importsegmentation/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:
@@ -18,9 +21,11 @@ workflow PROSEG_PRESET_PROSEG2BAYSOR_TILED {
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]
@@ -36,14 +41,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])
}
@@ -82,6 +87,7 @@ workflow PROSEG_PRESET_PROSEG2BAYSOR_TILED {
XENIUMRANGER_IMPORTSEGMENTATION ( ch_xr )
emit:
+
coordinate_space = ch_coordinate_space // channel: [ "microns" ]
redefined_bundle = XENIUMRANGER_IMPORTSEGMENTATION.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 ab9ae090..bcd5f2d7 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'
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 f4786715..e6026c7c 100644
--- a/subworkflows/local/utils_nfcore_spatialaxe_pipeline/main.nf
+++ b/subworkflows/local/utils_nfcore_spatialaxe_pipeline/main.nf
@@ -278,14 +278,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 5b9979b4..78372d98 100644
--- a/tests/coordinate_mode.nf.test.snap
+++ b/tests/coordinate_mode.nf.test.snap
@@ -335,10 +335,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"
@@ -378,15 +378,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",
diff --git a/tests/default.nf.test.snap b/tests/default.nf.test.snap
index b24374ad..81d1b336 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",
diff --git a/tests/image_mode.nf.test.snap b/tests/image_mode.nf.test.snap
index 0c9770d6..47c906a0 100644
--- a/tests/image_mode.nf.test.snap
+++ b/tests/image_mode.nf.test.snap
@@ -43,9 +43,8 @@
"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",
@@ -117,8 +116,8 @@
],
[
"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",
diff --git a/tests/preview_mode.nf.test.snap b/tests/preview_mode.nf.test.snap
index 60f47591..5198374b 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/raw_bundle",
"preview/multiqc/raw_bundle/multiqc_data",
@@ -71,9 +70,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",
@@ -84,7 +83,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",
diff --git a/tests/segfree_mode.nf.test.snap b/tests/segfree_mode.nf.test.snap
index 02026e9e..03f146a2 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/raw_bundle",
"segfree/multiqc/raw_bundle/multiqc_data",
@@ -67,7 +66,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",
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 ed72343c..90057353 100644
--- a/workflows/spatialaxe.nf
+++ b/workflows/spatialaxe.nf
@@ -60,9 +60,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
@@ -108,26 +110,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()
@@ -292,11 +294,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) {
@@ -419,8 +430,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
}
@@ -455,12 +466,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,
@@ -468,6 +480,8 @@ workflow SPATIALAXE {
min_qv,
min_x,
min_y,
+ prior_column,
+ min_transcripts_per_cell,
nucleus_segmentation_only,
sharpen_tiff,
stardist_nuclei_model,
@@ -493,11 +507,14 @@ 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,
@@ -505,9 +522,9 @@ workflow SPATIALAXE {
min_y,
expansion_distance,
)
+ 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
@@ -576,7 +593,7 @@ workflow SPATIALAXE {
SEGGER_CREATE_TRAIN_PREDICT(
ch_bundle_path,
- ch_transcripts_file,
+ ch_transcripts_parquet,
segger_model,
expansion_distance,
)
@@ -597,14 +614,11 @@ 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,
@@ -681,13 +695,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
)
}
@@ -695,7 +709,7 @@ workflow SPATIALAXE {
if (method == 'ficture') {
FICTURE_PREPROCESS_MODEL(
- ch_transcripts_file,
+ ch_transcripts_parquet,
ch_features,
features,
)