diff --git a/scripts/create_resources/spatial/mirror_bruker_to_s3.sh b/scripts/create_resources/spatial/mirror_bruker_to_s3.sh new file mode 100644 index 000000000..4a02bf7db --- /dev/null +++ b/scripts/create_resources/spatial/mirror_bruker_to_s3.sh @@ -0,0 +1,134 @@ +#!/bin/bash + +# Mirror the Bruker CosMx raw zips from the (flaky) NanoString/liquidweb server to S3. +# +# The NanoString server frequently resets the connection mid-transfer, and these +# zips are enormous (HalfBrain ~175 GB, NormalLiver ~349 GB). Nextflow's foreign-file +# staging does NOT resume, so it dies on every run. Mirroring to S3 once makes future +# runs fast and deterministic. +# +# This script is resumable and idempotent: +# - curl -C - resumes a partial download after a connection reset +# - files already present in S3 (with matching size) are skipped +# - each file is deleted from local scratch after upload, so you only ever need +# free disk for the LARGEST single file (~350 GB), not the sum +# +# Just re-run it if it dies; it picks up where it left off. +# +# Usage: +# SCRATCH_DIR=/path/to/big/scratch ./mirror_bruker_to_s3.sh + +set -euo pipefail + +# --- Config ----------------------------------------------------------------- + +SRC_BASE="https://smi-public.objects.liquidweb.services" +S3_DEST="${S3_DEST:-s3://openproblems-data/resources_raw/bruker_cosmx}" +SCRATCH_DIR="${SCRATCH_DIR:-$PWD/bruker_mirror_scratch}" + +# Files to mirror. The URL-encoded names are what the server serves; the second +# column is the (decoded) name to store under on S3. +FILES=( + "HalfBrain.zip|HalfBrain.zip" + "Half%20%20Brain%20simple%20%20files%20.zip|Half Brain simple files.zip" + "NormalLiverFiles.zip|NormalLiverFiles.zip" +) + +# --- Preflight -------------------------------------------------------------- + +command -v curl >/dev/null || { echo "ERROR: curl not found" >&2; exit 1; } +command -v aws >/dev/null || { echo "ERROR: aws CLI not found" >&2; exit 1; } + +mkdir -p "$SCRATCH_DIR" +echo "Scratch dir : $SCRATCH_DIR" +echo "S3 dest : $S3_DEST" +echo "Free space :" +df -h "$SCRATCH_DIR" | sed 's/^/ /' +echo + +remote_size() { + # Content-Length of the remote file, or empty if unavailable + curl -sSL -I --max-time 60 "$1" \ + | tr -d '\r' \ + | awk 'tolower($1)=="content-length:"{print $2}' \ + | tail -n1 +} + +s3_size() { + # Size of the object in S3, or empty if it doesn't exist + aws s3api head-object --bucket "$1" --key "$2" --query 'ContentLength' --output text 2>/dev/null || true +} + +# --- Main loop -------------------------------------------------------------- + +for entry in "${FILES[@]}"; do + url_name="${entry%%|*}" + local_name="${entry##*|}" + url="$SRC_BASE/$url_name" + local_path="$SCRATCH_DIR/$local_name" + + # S3 key = everything after s3://bucket/ + bucket="$(echo "$S3_DEST" | sed -E 's#^s3://([^/]+)/.*#\1#')" + prefix="$(echo "$S3_DEST" | sed -E 's#^s3://[^/]+/(.*)#\1#')" + key="$prefix/$local_name" + + echo "================================================================" + echo "File: $local_name" + echo " URL: $url" + echo " S3 : s3://$bucket/$key" + + expected="$(remote_size "$url")" + if [[ -z "$expected" ]]; then + echo " WARN: could not read remote Content-Length; proceeding without size checks" + else + echo " Remote size: $expected bytes ($(awk -v b="$expected" 'BEGIN{printf "%.1f GB", b/1024/1024/1024}'))" + fi + + # Skip if already uploaded with the right size + existing="$(s3_size "$bucket" "$key")" + if [[ -n "$existing" && "$existing" != "None" ]]; then + if [[ -z "$expected" || "$existing" == "$expected" ]]; then + echo " SKIP: already in S3 (size $existing bytes)" + continue + else + echo " Present in S3 but size mismatch ($existing != $expected); re-uploading" + fi + fi + + # Download with resume + aggressive retry on connection resets + echo " Downloading (resumable)..." + curl -L -C - \ + --retry 100 --retry-all-errors --retry-delay 10 \ + --connect-timeout 60 \ + -o "$local_path" \ + "$url" + + # Verify size before uploading + if [[ -n "$expected" ]]; then + actual="$(stat -c%s "$local_path" 2>/dev/null || stat -f%z "$local_path")" + if [[ "$actual" != "$expected" ]]; then + echo " ERROR: downloaded size $actual != expected $expected. Re-run to resume." >&2 + exit 1 + fi + echo " Size verified: $actual bytes" + fi + + # Upload to S3 + echo " Uploading to s3://$bucket/$key ..." + aws s3 cp "$local_path" "s3://$bucket/$key" + + # Free scratch space before the next (much larger) file + echo " Removing local copy to free space" + rm -f "$local_path" + echo " Done: $local_name" +done + +echo "================================================================" +echo "All files mirrored to $S3_DEST" +echo +echo "Next: update the input_raw / input_flat_files URLs in" +echo " scripts/create_resources/spatial/process_bruker_cosmx_nebius.sh" +echo "to point at the S3 paths, e.g.:" +echo " input_raw: $S3_DEST/HalfBrain.zip" +echo " input_flat_files: $S3_DEST/Half Brain simple files.zip" +echo " input_raw: $S3_DEST/NormalLiverFiles.zip" diff --git a/scripts/create_resources/spatial/process_10x_atera_nebius.sh b/scripts/create_resources/spatial/process_10x_atera_nebius.sh index d3475e586..432122295 100644 --- a/scripts/create_resources/spatial/process_10x_atera_nebius.sh +++ b/scripts/create_resources/spatial/process_10x_atera_nebius.sh @@ -8,7 +8,9 @@ cd "$REPO_ROOT" set -e -publish_dir="s3://openproblems-data/resources/datasets" +# store the loader output locally, mirroring the process_datasets layout ($id/) +# but under a sibling raw/ folder +publish_dir="/scratch/task_ist_preprocessing/raw" cat > /tmp/params_atera.yaml << HERE param_list: diff --git a/src/base/labels_nebius.config b/src/base/labels_nebius.config index fd98f257d..c5edf9719 100644 --- a/src/base/labels_nebius.config +++ b/src/base/labels_nebius.config @@ -47,20 +47,26 @@ process { withLabel: midcpu { cpus = 15 } withLabel: highcpu { cpus = 30 } withLabel: lowmem { - memory = { get_memory( 20.GB * task.attempt ) } + // Nebius 48vcpu-192gb nodes: 188 GB allocatable + memory = { get_memory( 25.GB * task.attempt ) } disk = { 50.GB * task.attempt } + pod = [[nodeSelector: 'nebius.com/node-group-id=mk8snodegroup-e00p5wjvb7bc55b2js']] } withLabel: midmem { + // Nebius 64vcpu-256gb nodes: 251 GB allocatable memory = { get_memory( 50.GB * task.attempt ) } disk = { 100.GB * task.attempt } + pod = [[nodeSelector: 'nebius.com/node-group-id=mk8snodegroup-e00wvqfyde1nfwezs0']] } withLabel: highmem { - memory = { task.attempt == 1 ? 200.GB : 500.GB } + // Nebius 128vcpu-512gb nodes: 503 GB allocatable — cap well below node max + memory = { task.attempt == 1 ? 200.GB : 450.GB } disk = 200.GB pod = [[nodeSelector: 'nebius.com/node-group-id=mk8snodegroup-e00hnqhyfdcsy9m09n']] } withLabel: veryhighmem { - memory = { get_memory( 200.GB * task.attempt ) } + // Nebius 128vcpu-512gb nodes: 503 GB allocatable — cap at 450 GB to leave headroom + memory = { [ 200.GB * task.attempt, 450.GB ].min() } disk = { 400.GB * task.attempt } pod = [[nodeSelector: 'nebius.com/node-group-id=mk8snodegroup-e00hnqhyfdcsy9m09n']] } @@ -115,6 +121,28 @@ process { containerOptions = { workflow.containerEngine == "singularity" ? '--nv': ( workflow.containerEngine == "docker" ? '--gpus all': null ) } } + withLabel: gpuhighmem { + // Nebius high-mem gpu-node-group: NVIDIA L40S, 1 GPU, 40 CPUs, 160 GiB RAM + // ~156 GiB allocatable — cap memory at 150 GB to leave headroom + cpus = 30 + accelerator = 1 + memory = { [ 100.GB * task.attempt, 150.GB ].min() } + disk = 200.GB + pod = [[nodeSelector: 'nebius.com/node-group-id=mk8snodegroup-e00dhcgx1xqjskycvc']] + containerOptions = { workflow.containerEngine == "singularity" ? '--nv': + ( workflow.containerEngine == "docker" ? '--gpus all': null ) } + } + withLabel: gpuh100 { + // Nebius H100 gpu-node-group: NVIDIA H100 SXM, 1 GPU, 16 CPUs, 196 GiB RAM + // ~195 GiB / ~15.9 vCPU allocatable — cap well below node max to leave headroom + cpus = 12 + accelerator = 1 + memory = { [ 120.GB * task.attempt, 180.GB ].min() } + disk = 200.GB + pod = [[nodeSelector: 'nebius.com/node-group-id=mk8snodegroup-e00jp7hyqr094tmy35']] + containerOptions = { workflow.containerEngine == "singularity" ? '--nv': + ( workflow.containerEngine == "docker" ? '--gpus all': null ) } + } withLabel: hightime { time = 12.h } withLabel: veryhightime { time = 24.h } diff --git a/src/datasets/loaders/allen_brain_cell_atlas_merfish/script.py b/src/datasets/loaders/allen_brain_cell_atlas_merfish/script.py index 014775282..f9e4fe979 100644 --- a/src/datasets/loaders/allen_brain_cell_atlas_merfish/script.py +++ b/src/datasets/loaders/allen_brain_cell_atlas_merfish/script.py @@ -1,6 +1,7 @@ import math import os import re +import time import urllib.request from datetime import datetime from pathlib import Path @@ -137,8 +138,38 @@ def close(self): self.fileptr.close() +def robust_urlretrieve(url, path, retries=5, backoff=5): + """urlretrieve that retries on transient network errors. + + Returns True on success, False if the file is genuinely absent (404) or + every retry was exhausted. Partial downloads are cleaned up so a later + ``.exists()`` check does not treat a truncated file as complete. + """ + for attempt in range(1, retries + 1): + try: + urllib.request.urlretrieve(url, path) + return True + except urllib.error.HTTPError as e: + if e.code == 404: + print(datetime.now() - t0, f"Not found (404): {url}", flush=True) + return False + print(datetime.now() - t0, + f"HTTP {e.code} (attempt {attempt}/{retries}) for {url}", flush=True) + except (urllib.error.URLError, ConnectionError, TimeoutError) as e: + print(datetime.now() - t0, + f"Download failed (attempt {attempt}/{retries}) for {url}: {e}", flush=True) + Path(path).unlink(missing_ok=True) + if attempt < retries: + time.sleep(backoff * attempt) + return False + + def download_fov_image(fov_idx, n_format, prefix, suffix, base_url, frames, stain): - """Download one FOV dax file and return a (n_z, px, py) uint16 array.""" + """Download one FOV dax file and return a (n_z, px, py) uint16 array. + + Returns None if the FOV image cannot be fetched (missing file or persistent + network failure) so the caller can skip it instead of crashing the run. + """ fmt = f"{fov_idx:03d}" if n_format == 3 else f"{fov_idx:04d}" stem = prefix + fmt + suffix dax_path = TMP_DIR / (stem + ".dax") @@ -149,8 +180,11 @@ def download_fov_image(fov_idx, n_format, prefix, suffix, base_url, frames, stai return tifffile.imread(tif_path) if not dax_path.exists(): - urllib.request.urlretrieve(base_url + stem + ".dax", dax_path) - urllib.request.urlretrieve(base_url + stem + ".inf", inf_path) + if not robust_urlretrieve(base_url + stem + ".dax", dax_path): + return None + if not robust_urlretrieve(base_url + stem + ".inf", inf_path): + dax_path.unlink(missing_ok=True) + return None reader = DaxReader(str(dax_path)) img = np.array([reader.load_frame(f) for f in frames]) @@ -288,6 +322,9 @@ def um_to_px_y(y): yp, yp_max = int(row["y_min"]), int(row["y_max"]) fov_img = download_fov_image(fov, n_format, dapi_file_prefix, download_suffix, image_base_url, dapi_frames, "DAPI") + if fov_img is None: + print(datetime.now() - t0, f"Skipping FOV {fov}: image unavailable", flush=True) + continue # Find neighbors for linear blending at overlaps fov_right = fov_df[ @@ -304,8 +341,11 @@ def um_to_px_y(y): right = len(fov_right) > 0 and fov_right[0] not in missing_fovs bottom = len(fov_bottom) > 0 and fov_bottom[0] not in missing_fovs + bot_img = right_img = None if bottom: bot_img = download_fov_image(int(fov_bottom[0]), n_format, dapi_file_prefix, download_suffix, image_base_url, dapi_frames, "DAPI") + bottom = bot_img is not None + if bottom: fov_img[:, FRAME_SIZE_PIXELS - OVERLAP_SIZE:, :] = ( np.multiply(fov_img[:, FRAME_SIZE_PIXELS - OVERLAP_SIZE:, :], w1) + np.multiply(bot_img[:, :OVERLAP_SIZE, :], w2) @@ -313,13 +353,17 @@ def um_to_px_y(y): if right: right_img = download_fov_image(int(fov_right[0]), n_format, dapi_file_prefix, download_suffix, image_base_url, dapi_frames, "DAPI") + right = right_img is not None + if right: fov_img[:, :, FRAME_SIZE_PIXELS - OVERLAP_SIZE:] = ( np.multiply(fov_img[:, :, FRAME_SIZE_PIXELS - OVERLAP_SIZE:], w1.T) + np.multiply(right_img[:, :, :OVERLAP_SIZE], w2.T) ) / (w1.T + w2.T) + br_img = None if len(fov_br) > 0 and fov_br[0] not in missing_fovs: br_img = download_fov_image(int(fov_br[0]), n_format, dapi_file_prefix, download_suffix, image_base_url, dapi_frames, "DAPI") + if br_img is not None: sl_x = slice(FRAME_SIZE_PIXELS - OVERLAP_SIZE, None) sl_y = slice(FRAME_SIZE_PIXELS - OVERLAP_SIZE, None) if bottom and right: diff --git a/src/datasets/loaders/tenx_atera/config.vsh.yaml b/src/datasets/loaders/tenx_atera/config.vsh.yaml index 6da3a43bf..ed74ffeac 100644 --- a/src/datasets/loaders/tenx_atera/config.vsh.yaml +++ b/src/datasets/loaders/tenx_atera/config.vsh.yaml @@ -68,4 +68,4 @@ runners: - type: executable - type: nextflow directives: - label: [midmem, midcpu, midtime] \ No newline at end of file + label: [highmem, midcpu, midtime] \ No newline at end of file diff --git a/src/methods_calculate_cell_volume/alpha_shapes/script.py b/src/methods_calculate_cell_volume/alpha_shapes/script.py index 087a0f1b9..f54c8fdf5 100644 --- a/src/methods_calculate_cell_volume/alpha_shapes/script.py +++ b/src/methods_calculate_cell_volume/alpha_shapes/script.py @@ -15,7 +15,7 @@ sdata = sd.read_zarr(par['input']) print('Determine cell ids', flush=True) -cell_ids = sorted(sdata["transcripts"]["cell_id"].unique()) +cell_ids = sorted(sdata["transcripts"]["cell_id"].unique().compute()) if cell_ids[0] == 0: cell_ids = cell_ids[1:] diff --git a/src/methods_cell_type_annotation/moscot/config.vsh.yaml b/src/methods_cell_type_annotation/moscot/config.vsh.yaml index 27a02cedc..ea79aa1b5 100644 --- a/src/methods_cell_type_annotation/moscot/config.vsh.yaml +++ b/src/methods_cell_type_annotation/moscot/config.vsh.yaml @@ -57,4 +57,4 @@ runners: - type: executable - type: nextflow directives: - label: [ hightime, midcpu, veryhighmem, gpu ] + label: [ hightime, midcpu, veryhighmem, gpuh100 ] diff --git a/src/methods_cell_type_annotation/tangram/config.vsh.yaml b/src/methods_cell_type_annotation/tangram/config.vsh.yaml index 447a3617c..903c82c52 100644 --- a/src/methods_cell_type_annotation/tangram/config.vsh.yaml +++ b/src/methods_cell_type_annotation/tangram/config.vsh.yaml @@ -40,4 +40,4 @@ runners: - type: executable - type: nextflow directives: - label: [ midtime, midcpu, midmem, gpu ] + label: [ midtime, midcpu, midmem, gpuh100 ] diff --git a/src/methods_segmentation/cellpose/config.vsh.yaml b/src/methods_segmentation/cellpose/config.vsh.yaml index 1187c12a9..e0aad23bd 100644 --- a/src/methods_segmentation/cellpose/config.vsh.yaml +++ b/src/methods_segmentation/cellpose/config.vsh.yaml @@ -93,4 +93,4 @@ runners: - type: executable - type: nextflow directives: - label: [ midtime, midcpu, highmem, gpu ] + label: [ midtime, midcpu, highmem, gpuhighmem ] diff --git a/src/methods_segmentation/cellpose/script.py b/src/methods_segmentation/cellpose/script.py index 190f80da3..2f33dd2aa 100644 --- a/src/methods_segmentation/cellpose/script.py +++ b/src/methods_segmentation/cellpose/script.py @@ -47,9 +47,25 @@ def convert_to_lower_dtype(arr): parsed_data = Labels2DModel.parse(data_array, transformations=transformation) sd_output.labels['segmentation'] = parsed_data +metadata = sdata.tables["metadata"] +# cell_id is required downstream. Standard Xenium exports carry an explicit +# "cell_id" column, but some exports (e.g. the Xenium WTA preview used for the +# Atera dataset) don't — there the per-cell identifier lives in the table's +# instance_key column (falling back to the obs index). +instance_key = metadata.uns.get("spatialdata_attrs", {}).get("instance_key") +if "cell_id" in metadata.obs.columns: + cell_id = metadata.obs["cell_id"].values +elif instance_key and instance_key in metadata.obs.columns: + cell_id = metadata.obs[instance_key].values +else: + cell_id = metadata.obs.index.values +obs = metadata.obs[[]].copy() +obs["cell_id"] = cell_id +if "region" in metadata.obs.columns: + obs["region"] = metadata.obs["region"].values sd_output.tables['table'] = ad.AnnData( - obs=sdata.tables["metadata"].obs[["cell_id", "region"]], - var=sdata.tables["metadata"].var[[]] + obs=obs, + var=metadata.var[[]] ) print("Writing output", flush=True) diff --git a/src/methods_segmentation/cellposev4/config.vsh.yaml b/src/methods_segmentation/cellposev4/config.vsh.yaml new file mode 100644 index 000000000..25d20d264 --- /dev/null +++ b/src/methods_segmentation/cellposev4/config.vsh.yaml @@ -0,0 +1,61 @@ +name: cellposev4 +label: "Cellpose 4 Segmentation" +summary: "Cell segmentation with Cellpose 4 (CellposeModel / cpsam)." +description: | + Cellpose predicts, for every pixel, a vector pointing toward the center of the cell it belongs to + together with a probability of being part of a cell. These per-pixel vectors form a flow field; + at inference each pixel follows the flow to a fixed point, and pixels converging to the same point + are grouped into a shared mask. This is the Cellpose 4 (cellpose>=4.0.0) implementation, which uses + the built-in CellposeModel (cpsam) network via model.eval, adapted from + openproblems-bio/task_spatial_segmentation. +links: + documentation: "https://cellpose.readthedocs.io/en/latest/" + repository: "https://github.com/mouseland/cellpose" +references: + doi: "10.1038/s41592-020-01018-x" + +__merge__: /src/api/comp_method_segmentation.yaml + +arguments: + - name: --diameter + type: double + default: 30.0 + description: "Cell diameter in pixels. If not set, cellpose runs a size model to estimate it (slower)." + - name: --flow_threshold + type: double + default: 0.0 + description: "Flow error threshold. Set to 0 to skip flow quality check for faster execution." + - name: --niter + type: integer + default: 10 + description: "Number of iterations for dynamics. Lower values are faster but less accurate." + - name: --min_size + type: integer + default: -1 + description: "Minimum number of pixels per mask. Set to -1 to skip small mask removal." + - name: --resample + type: boolean + default: false + description: "Whether to run dynamics at original image size. Disabling is faster." + +resources: + - type: python_script + path: script.py + +engines: + - type: docker + image: openproblems/base_pytorch_nvidia:1 + setup: + - type: python + pypi: cellpose>=4.0.0 + - type: python + script: from cellpose.models import CellposeModel; model = CellposeModel() + __merge__: + - /src/base/setup_spatialdata_partial.yaml + - type: native + +runners: + - type: executable + - type: nextflow + directives: + label: [ midtime, midcpu, highmem, gpuhighmem ] diff --git a/src/methods_segmentation/cellposev4/script.py b/src/methods_segmentation/cellposev4/script.py new file mode 100644 index 000000000..1c5e51a7f --- /dev/null +++ b/src/methods_segmentation/cellposev4/script.py @@ -0,0 +1,90 @@ +import anndata as ad +import numpy as np +import os +import shutil +import spatialdata as sd +import xarray as xr +from cellpose.models import CellposeModel +from spatialdata.models import Labels2DModel +import torch + +# Check whether a GPU is available +device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') +print('Using device:', device, flush=True) + +## VIASH START +par = { + "input": "resources_test/task_ist_preprocessing/mouse_brain_combined/raw_ist.zarr", + "output": "segmentation.zarr", + "diameter": 30.0, + "flow_threshold": 0.0, + "niter": 10, + "min_size": -1, + "resample": False, +} +meta = { + "name": "cellposev4", +} +## VIASH END + + +def convert_to_lower_dtype(arr): + max_val = arr.max() + if max_val <= np.iinfo(np.uint8).max: + new_dtype = np.uint8 + elif max_val <= np.iinfo(np.uint16).max: + new_dtype = np.uint16 + elif max_val <= np.iinfo(np.uint32).max: + new_dtype = np.uint32 + else: + new_dtype = np.uint64 + + return arr.astype(new_dtype) + + +print('Reading input', flush=True) +sdata = sd.read_zarr(par["input"]) +image = sdata['image']['scale0'].image.compute().to_numpy() +transformation = sdata['image']['scale0'].image.transform.copy() + +print('Initializing Cellpose model', flush=True) +model = CellposeModel(gpu=torch.cuda.is_available()) + +eval_params = {k: par[k] for k in ("diameter", "flow_threshold", "niter", "min_size", "resample") if par.get(k) is not None} +print('Running Cellpose segmentation with parameters:', eval_params, flush=True) +masks, _, _ = model.eval(image[0], progress=True, **eval_params) + +print('Cellpose segmentation finished, post-processing results', flush=True) +masks = convert_to_lower_dtype(masks) + +data_array = xr.DataArray(masks, name='segmentation', dims=('y', 'x')) +parsed_data = Labels2DModel.parse(data_array, transformations=transformation) + +sd_output = sd.SpatialData() +sd_output.labels['segmentation'] = parsed_data + +metadata = sdata.tables["metadata"] +# cell_id is required downstream. Standard Xenium exports carry an explicit +# "cell_id" column, but some exports (e.g. the Xenium WTA preview used for the +# Atera dataset) don't — there the per-cell identifier lives in the table's +# instance_key column (falling back to the obs index). +instance_key = metadata.uns.get("spatialdata_attrs", {}).get("instance_key") +if "cell_id" in metadata.obs.columns: + cell_id = metadata.obs["cell_id"].values +elif instance_key and instance_key in metadata.obs.columns: + cell_id = metadata.obs[instance_key].values +else: + cell_id = metadata.obs.index.values +obs = metadata.obs[[]].copy() +obs["cell_id"] = cell_id +if "region" in metadata.obs.columns: + obs["region"] = metadata.obs["region"].values +sd_output.tables['table'] = ad.AnnData( + obs=obs, + var=metadata.var[[]] + ) + +print("Writing output", flush=True) +if os.path.exists(par["output"]): + shutil.rmtree(par["output"]) +sd_output.write(par["output"]) diff --git a/src/workflows/run_benchmark/config.vsh.yaml b/src/workflows/run_benchmark/config.vsh.yaml index 665d540fc..dc79d85e2 100644 --- a/src/workflows/run_benchmark/config.vsh.yaml +++ b/src/workflows/run_benchmark/config.vsh.yaml @@ -62,7 +62,7 @@ argument_groups: A list of segmentation methods to run. type: string multiple: true - default: "custom_segmentation:cellpose:binning:stardist:watershed" + default: "custom_segmentation:cellpose:cellposev4:binning:stardist:watershed" - name: "--transcript_assignment_methods" description: | A list of transcript assignment methods to run. @@ -150,6 +150,7 @@ dependencies: - name: control_methods/permute_celltype_annotations - name: methods_segmentation/custom_segmentation - name: methods_segmentation/cellpose + - name: methods_segmentation/cellposev4 - name: methods_segmentation/binning - name: methods_segmentation/stardist - name: methods_segmentation/watershed diff --git a/src/workflows/run_benchmark/main.nf b/src/workflows/run_benchmark/main.nf index 6ab1f6d31..64cfc7f82 100644 --- a/src/workflows/run_benchmark/main.nf +++ b/src/workflows/run_benchmark/main.nf @@ -95,6 +95,7 @@ workflow run_wf { args: [labels_key: "cell_labels"] ), cellpose, + cellposev4, binning, stardist, watershed