diff --git a/.github/workflows/nf-test.yml b/.github/workflows/nf-test.yml index 5dc5a547..8abe0187 100644 --- a/.github/workflows/nf-test.yml +++ b/.github/workflows/nf-test.yml @@ -81,7 +81,7 @@ jobs: - isMain: true profile: "conda" NXF_VER: - - "25.04.0" + - "26.04.6" - "latest-everything" env: NXF_ANSI_LOG: false diff --git a/CHANGELOG.md b/CHANGELOG.md index c3dbe4cf..04de5b1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,12 +9,31 @@ Initial release of nf-core/spatialaxe, created with the [nf-core](https://nf-co. ### `Added` +- Added the entire **spoQC tool and subworkflow**: `subworkflows/local/spoqc/main.nf` plus 31 new local modules under `modules/local/spoQC/` (`ambient`, `analysis_category`, `analysis_cluster`, `analysis_overview`, `annotation`, `bubble`, `cell`, `cellcycle`, `combine_masks`, `doublet`, `finalreport`, `general`, `hqcr_celltype`, `hqcr_ident`, `hqpr_bounding_box`, `hqpr_celltype`, `hqpr_clustering`, `hqpr_metrices`, `hqpr_refinement`, `hqtr_ac`, `hqtr_bounding_box`, `hqtr_celltype`, `hqtr_clustering`, `hqtr_metrices`, `hqtr_qv`, `hqtr_refinement`, `marker`, `model`, `transcript`, `void`, `whole_slide`), each with its own `main.nf`, `meta.yml`, and nf-test suite (`tests/main.nf.test` + snapshot). +- Wired spoQC into `workflows/spatialaxe.nf`. +- Added `bin/spatialdata_write.py` support for spoQC's SpatialData output. +- New test configs: `conf/tests/test_spoqc.config` and `conf/tests/test_full_spoqc.config` +- New samplesheet for a full test: `assets/samplesheet_full.csv` with `conf/test_full.config`. +- `conf/base.config`: adding combinatorial label system — separate `process_{tiny,low,mid,high,xl}_cpus`, `_mem`, and `_time` labels, each scaling with `task.attempt`. +- Added `nf-core/unzip` module usage so the pipeline can unpack the larger test dataset. +- samplesheet redefinition: `sample,bundle,image,annotation,stainings`, samplesheet allows for two additional optional columns `annotation,stainings` that are useful for the QC subworkflow. +- `spatialdata_write_meta_merge/main.nf`: Change to subworkflow to account for proper `qc` mode. +- Change to `bin/spatialdata_write.py`: Adding an `all` mode to set all available features to `True`, which is important for QC. + ### `Fixed` +- Change to input validation in `spatialaxe.nf`: Moving `morphology_focus/` folder into `bundle_optional_files` because not all Xenium bundles have such a folder. + ### `Dependencies` ### `Deprecated` +- `nextflow.config`: bumped the `nf-schema` plugin version. +- `subworkflows/nf-core/utils_nfschema_plugin/main.nf`: added a new `cli_typecast` input (pass `null` to keep default behavior), renamed the `parametersSchema` option key to `parameters_schema` across the help/summary/validate option maps, and fixed how the `--help` text value is resolved. +- `subworkflows/nf-core/utils_nextflow_pipeline/main.nf`: bumped the version. +- `subworkflows/local/utils_nfcore_spatialaxe_pipeline/main.nf`: passes the new `cli_typecast` argument (`null`) through to `UTILS_NFSCHEMA_PLUGIN`. +- `nextflow_schema.json`: cleanup driven by the new schema version. + ## 1.0.1 - [06.08.2026] Hotfix to tackle some bugs diff --git a/README.md b/README.md index ba9c04fd..ceb80ef3 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ [![GitHub Actions Linting Status](https://github.com/nf-core/spatialaxe/actions/workflows/linting.yml/badge.svg)](https://github.com/nf-core/spatialaxe/actions/workflows/linting.yml)[![AWS CI](https://img.shields.io/badge/CI%20tests-full%20size-FF9900?labelColor=000000&logo=Amazon%20AWS)](https://nf-co.re/spatialaxe/results)[![Cite with Zenodo](http://img.shields.io/badge/DOI-10.5281/zenodo.20733817-1073c8?labelColor=000000)](https://doi.org/10.5281/zenodo.20733817) [![nf-test](https://img.shields.io/badge/unit_tests-nf--test-337ab7.svg)](https://www.nf-test.com) -[![Nextflow](https://img.shields.io/badge/version-%E2%89%A525.04.0-green?style=flat&logo=nextflow&logoColor=white&color=%230DC09D&link=https%3A%2F%2Fnextflow.io)](https://www.nextflow.io/) +[![Nextflow](https://img.shields.io/badge/version-%E2%89%A526.04.6-green?style=flat&logo=nextflow&logoColor=white&color=%230DC09D&link=https%3A%2F%2Fnextflow.io)](https://www.nextflow.io/) [![nf-core template version](https://img.shields.io/badge/nf--core_template-3.4.1-green?style=flat&logo=nfcore&logoColor=white&color=%2324B064&link=https%3A%2F%2Fnf-co.re)](https://github.com/nf-core/tools/releases/tag/3.4.1) [![run with docker](https://img.shields.io/badge/run%20with-docker-0db7ed?labelColor=000000&logo=docker)](https://www.docker.com/) [![run with singularity](https://img.shields.io/badge/run%20with-singularity-1d355c.svg?labelColor=000000)](https://sylabs.io/docs/) @@ -48,6 +48,7 @@ The pipeline supports the following tools: - QC methods: - [MultiQC Xenium Extra Plugin](https://github.com/MultiQC/xenium-extra) - [OPT](https://github.com/JEFworks-Lab/off-target-probe-tracker) + - [spoQC](https://github.com/heylf/spoQC) ## Usage diff --git a/assets/samplesheet.csv b/assets/samplesheet.csv index 72ad34f0..74b5ac2f 100644 --- a/assets/samplesheet.csv +++ b/assets/samplesheet.csv @@ -1,2 +1,2 @@ -sample,bundle,image -test_run,https://raw.githubusercontent.com/nf-core/test-datasets/spatialaxe/xenium_bundle.tar.gz, +sample,bundle,image,annotation,stainings +test_run,https://raw.githubusercontent.com/nf-core/test-datasets/spatialaxe/xenium_bundle.tar.gz,,,0; diff --git a/assets/samplesheet_full.csv b/assets/samplesheet_full.csv new file mode 100644 index 00000000..7ba1748f --- /dev/null +++ b/assets/samplesheet_full.csv @@ -0,0 +1,2 @@ +sample,bundle,image,annotation,stainings +test_run_full,https://raw.githubusercontent.com/nf-core/test-datasets/spatialxe/Xenium_V1_Protein_Human_Kidney_tiny_outs.zip,,,0;1 diff --git a/assets/schema_input.json b/assets/schema_input.json index c6cda688..fb7f1ddf 100644 --- a/assets/schema_input.json +++ b/assets/schema_input.json @@ -22,6 +22,16 @@ "type": "string", "pattern": "^\\S+$", "errorMessage": "You can provide an image. If you do not then please leave the field empty." + }, + "annotation": { + "type": "string", + "pattern": "^\\S+$", + "errorMessage": "You can provide an annotation. If you do not then please leave the field empty." + }, + "stainings": { + "type": "string", + "pattern": "^\\S+$", + "errorMessage": "The indices of the image channels of the individual stainings you want to analyse." } }, "required": ["sample", "bundle"] diff --git a/bin/spatialdata_write.py b/bin/spatialdata_write.py index 3a4723e0..12d845db 100755 --- a/bin/spatialdata_write.py +++ b/bin/spatialdata_write.py @@ -130,6 +130,17 @@ def main(): nucleus_labels = False cells_as_circles = False + # set sd variables based on the coordinate space + if args.coordinate_space == "all": + cells_labels = True + nucleus_labels = True + cells_boundaries = True + nucleus_boundaries = True + cells_as_circles = True + + print(f"[NOTE] Apply cells_boundaries {cells_boundaries}") + print(f"[NOTE] Apply nucleus_boundaries {nucleus_boundaries}") + if args.format == "xenium": sd_xenium_obj = xenium( args.bundle, diff --git a/conf/base.config b/conf/base.config index 476c4dbe..8cd5a228 100644 --- a/conf/base.config +++ b/conf/base.config @@ -80,6 +80,82 @@ process { maxRetries = 2 } + // ========================================================================= + // Combinatorial CPU labels + // ========================================================================= + + // ========================= + // CPU + // ========================= + + withLabel:process_tiny_cpu { + cpus = { 1 * task.attempt } + } + + withLabel:process_low_cpus { + cpus = { 4 * task.attempt } + } + + withLabel:process_mid_cpus { + cpus = { 8 * task.attempt } + } + + withLabel:process_high_cpus { + cpus = { 12 * task.attempt } + } + + withLabel:process_xl_cpus { + cpus = { 30 * task.attempt } + } + + // ========================= + // Memory + // ========================= + + withLabel:process_tiny_mem { + memory = { 10.GB * task.attempt } + } + + withLabel:process_low_mem { + memory = { 30.GB * task.attempt } + } + + withLabel:process_mid_mem { + memory = { 50.GB * task.attempt } + } + + withLabel:process_high_mem { + memory = { 100.GB * task.attempt } + } + + withLabel:process_xl_mem { + memory = { 250.GB * task.attempt } + } + + // ========================= + // Time + // ========================= + + withLabel:process_tiny_time { + time = { 4.h * task.attempt } + } + + withLabel:process_low_time { + time = { 8.h * task.attempt } + } + + withLabel:process_mid_time { + time = { 16.h * task.attempt } + } + + withLabel:process_high_time { + time = { 24.h * task.attempt } + } + + withLabel:process_xl_time { + time = { 72.h * task.attempt } + } + // ========================================================================= // GPU labels // ========================================================================= @@ -100,4 +176,18 @@ process { time = { 16.h * task.attempt } } + // ========================================================================= + // Extra labels + // ========================================================================= + + // That needs to be included for some processes, esle kalleido failes because of tmp dir issues. + withLabel:spoqc { + beforeScript = { + """ + export TMPDIR=\${SLURM_TMPDIR:-\$NXF_TASK_WORKDIR/tmp} + mkdir -p \$TMPDIR + """ + } + } + } diff --git a/conf/modules.config b/conf/modules.config index 81cf6a25..51860d55 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -378,4 +378,13 @@ process { mode: params.publish_dir_mode, ] } + + // ---------------------------- spoQC ----------------------------------------- + + withName: '.*SPOQC_.*' { + publishDir = [ + path: { "${params.outdir}/spoqc/${meta.id}" }, + mode: params.publish_dir_mode, + ] + } } diff --git a/conf/test_full.config b/conf/test_full.config index fc94c8fd..300de3df 100644 --- a/conf/test_full.config +++ b/conf/test_full.config @@ -15,6 +15,7 @@ params { config_profile_description = 'Full test dataset to check pipeline function' // Input data - input = "${projectDir}/assets/samplesheet.csv" + input = "${projectDir}/assets/samplesheet_full.csv" + outdir = 'results' mode = 'coordinate' } diff --git a/conf/tests/test_full_spoqc.config b/conf/tests/test_full_spoqc.config new file mode 100644 index 00000000..1915d8e0 --- /dev/null +++ b/conf/tests/test_full_spoqc.config @@ -0,0 +1,35 @@ +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Nextflow config file for running minimal tests +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Defines input files and everything required to run a fast and simple pipeline test. + + Use as follows: + nextflow run nf-core/spatialaxe -profile test, --mode --outdir + +---------------------------------------------------------------------------------------- +*/ + +process { + + resourceLimits = [ + cpus: 4, + memory: '14.GB', + time: '2.h' + ] + + withName: UNTAR { + ext.prefix = "test_bundle" + } +} + +params { + config_profile_name = 'Test profile' + config_profile_description = 'Minimal test dataset to check pipeline function' + + // Input data + input = "${projectDir}/assets/samplesheet_full.csv" + outdir = 'results' + mode = 'qc' + spoqc = true +} diff --git a/conf/tests/test_spoqc.config b/conf/tests/test_spoqc.config new file mode 100644 index 00000000..adec107f --- /dev/null +++ b/conf/tests/test_spoqc.config @@ -0,0 +1,35 @@ +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Nextflow config file for running minimal tests +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Defines input files and everything required to run a fast and simple pipeline test. + + Use as follows: + nextflow run nf-core/spatialaxe -profile test, --mode --outdir + +---------------------------------------------------------------------------------------- +*/ + +process { + + resourceLimits = [ + cpus: 4, + memory: '14.GB', + time: '2.h' + ] + + withName: UNTAR { + ext.prefix = "test_bundle" + } +} + +params { + config_profile_name = 'Test profile' + config_profile_description = 'Minimal test dataset to check pipeline function' + + // Input data + input = "${projectDir}/assets/samplesheet.csv" + outdir = 'results' + mode = 'qc' + spoqc = true +} diff --git a/docs/output.md b/docs/output.md index 68c0256a..20442cb2 100644 --- a/docs/output.md +++ b/docs/output.md @@ -22,6 +22,10 @@ The pipeline is built using [Nextflow](https://www.nextflow.io/) and processes d - [MultiQC](#multiqc) - Aggregate report describing results and QC from the whole pipeline - [Pipeline information](#pipeline-information) - Report metrics generated during the workflow execution +## Quality Control + +[Quality Control](qc.md): An overview of the different QC reports. + ## Image mode
@@ -127,6 +131,55 @@ The pipeline is built using [Nextflow](https://www.nextflow.io/) and processes d - `*.tsv` TSV file containing the gene and transcript information to which each probe aligns - `stat/` - `*.tsv` TSV file containing the summary stats +- `spoqc/` + - `report/` + - `annotation/` + - `unsupervised_cell_annotation.tsv` unsupervised cell-type annotation, generated only when no annotation file was supplied as input + - `whole_slide_qc/` whole-slide QC overview report + - `generalqc/` general-purpose QC report + - `bubbleqc/` bubble-artifact QC report + - `doubletqc/` doublet-detection QC report + - `voidqc/` void/empty-region QC report + - `cellqc/` cell-level QC report + - `ambientqc/` ambient-RNA (background gene) QC report + - `hqcr/` + - `hqcr_ident/` high-quality cell region (HQCR) identification report + - `hqcr_celltype/` cell-type-refined HQCR report + - `hqpr/` + - `hqpr_metrices/${staining}/` per-staining high-quality pixel region (HQPR) metrics report + - `hqpr_clustering/${staining}/` per-staining HQPR clustering report + - `hqpr_refinement/${staining}/` per-staining HQPR mask refinement report + - `hqpr_bounding_box/${staining}/` per-staining HQPR bounding-box report + - `hqtr/` + - `hqtr_metrices/` high-quality transcript region (HQTR) metrics report + - `hqtr_ac/` HQTR ambient-contamination probability report + - `hqtr_qv/` HQTR quality-value probability report + - `hqtr_clustering/` HQTR clustering report + - `hqtr_refinement/` HQTR mask refinement report + - `hqtr_bounding_box/` HQTR bounding-box report + - `combine_masks/${staining}/` report combining the HQCR, HQPR and HQTR masks per staining + - `transcriptqc/` transcript-level QC report (against the reference gene panel) + - `cellcycleqc/` cell-cycle scoring QC report + - `modelqc/` model-based QC scoring report + - `analysis/` + - `overview/` downstream QC analysis overview report + - `rna_qc_annotated.h5ad` AnnData object annotated with the combined QC results + - `category/` downstream per-category QC analysis report + - `cluster/` downstream clustering QC analysis report + - `rna_cluster.h5ad` AnnData object with clustering results + - `staining_log.txt` log of the stainings processed by spoQC + - `report.html` final self-contained HTML report aggregating every spoQC step above + - `spoQC_tmp/` intermediate data consumed by later spoQC steps + - `generalqc_output_hqcr.parquet`, `bubbleqc_output_hqcr.parquet`, `doubletqc_output_hqcr.parquet`, `voidqc_output_hqcr.parquet`, `cellqc_output_hqcr.parquet` per-step HQCR contribution scores + - `ambient_output_genes.parquet` ambient RNA gene-signal estimate + - `hqcr_output_mask_raw.parquet` / `hqcr_output_mask_smoothed_raw.parquet` combined raw/smoothed HQCR mask + - `hqcr_output_mask_smoothed_celltype_refined.parquet` cell-type-refined smoothed HQCR mask + - `metrices/hqpr/${staining}/` per-staining HQPR metrics + - `hqpr_${staining}_output_mask_raw/` / `hqpr_${staining}_output_mask_smoothed_raw/` per-staining raw/smoothed HQPR mask + - `metrices/hqtr/` HQTR metrics + - `hqtr_output_ac_prob/` HQTR ambient-contamination probabilities + - `hqtr_output_qv_prob/` HQTR quality-value probabilities + - `hqtr_output_mask_raw/` / `hqtr_output_mask_smoothed_raw/` raw/smoothed HQTR mask - `multiqc/` - `multiqc_report.html`: a standalone HTML file that can be viewed in your web browser. - `multiqc_data/`: directory containing parsed statistics from the different tools used in the pipeline. @@ -164,7 +217,7 @@ The pipeline create spatialdata objects (data bundles) on various stages (see me
-### Xenium Ranger Import Segmentation) +### Xenium Ranger Import Segmentation This step is needed to import segemntations from different methods into the xenium bundle and is called at different stages of the pipeline. diff --git a/docs/qc.md b/docs/qc.md new file mode 100644 index 00000000..89ea8864 --- /dev/null +++ b/docs/qc.md @@ -0,0 +1,27 @@ +# nf-core/spatialaxe: QC + +## Introduction + +This document describes the QC reports produced by the pipeline. + +> [!NOTE] +> This is a work in progress. + +## QC methods overview + +The pipeline is built using [Nextflow](https://www.nextflow.io/) and inspects the quality of the data using the following methods: + +- [MultiQC Xenium Extra Plugin](https://github.com/MultiQC/xenium-extra) +- [OPT](https://github.com/JEFworks-Lab/off-target-probe-tracker) +- [spoQC](https://spoqc.readthedocs.io/en/latest/index.html) + +## MultiQC Xenium Extra Plugin + +## OPT + +## spoQC + +A full description of the report can be found here: + +- [Base report](https://spoqc.readthedocs.io/en/latest/output_report.html) +- [Additional report](https://spoqc.readthedocs.io/en/latest/output_report_additional.html) diff --git a/docs/usage.md b/docs/usage.md index 406f538a..062b3122 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -106,9 +106,21 @@ nextflow run nf-core/spatialaxe \ --mode qc ``` +To run [spoQC](https://github.com/heylf/spoQC) you have to set an additional option: + +```bash +nextflow run nf-core/spatialaxe \ + -profile \ + --input samplesheet.csv \ + --outdir \ + --mode qc \ + --spoqc true +``` + - QC methods: - [MultiQC Xenium Extra Plugin](https://github.com/MultiQC/xenium-extra) - [OPT](https://github.com/JEFworks-Lab/off-target-probe-tracker) + - [spoQC](https://github.com/heylf/spoQC) ### Image-based Segmentation mode (--mode image):
diff --git a/main.nf b/main.nf index dbbb24d4..15827672 100644 --- a/main.nf +++ b/main.nf @@ -79,6 +79,7 @@ workflow NFCORE_SPATIALAXE { params.stardist_nuclei_model, params.tiling, params.xeniumranger_only, + params.spoqc, ) emit: multiqc_report = SPATIALAXE.out.multiqc_report // channel: /path/to/multiqc_report.html diff --git a/modules.json b/modules.json index 494ffa18..6bcc4bde 100644 --- a/modules.json +++ b/modules.json @@ -49,13 +49,13 @@ }, "unzip": { "branch": "master", - "git_sha": "4dd9d8439a429c7ee566e0e2347f76ddeef27e66", + "git_sha": "1c43a7e22f04cf19c9f9d3de7e7e5a3addd9382a", "installed_by": ["modules"], "patch": "modules/nf-core/unzip/unzip.diff" }, "xeniumranger/importsegmentation": { "branch": "master", - "git_sha": "1fa9ed32cfd532e7d2191b5ffdcc86c783878fb0", + "git_sha": "39365e944e936511e33b993cdd978e0f12adac9a", "installed_by": ["modules"] }, "xeniumranger/relabel": { @@ -74,7 +74,7 @@ "nf-core": { "utils_nextflow_pipeline": { "branch": "master", - "git_sha": "05954dab2ff481bcb999f24455da29a5828af08d", + "git_sha": "1a545fcbd762911c21a64ced3dbef99b2b51ac75", "installed_by": ["subworkflows"] }, "utils_nfcore_pipeline": { @@ -84,7 +84,7 @@ }, "utils_nfschema_plugin": { "branch": "master", - "git_sha": "4b406a74dc0449c0401ed87d5bfff4252fd277fd", + "git_sha": "a7b27fd25bfa8dcc07d299e88bd790585901a436", "installed_by": ["subworkflows"] } } diff --git a/modules/local/spoQC/ambient/main.nf b/modules/local/spoQC/ambient/main.nf new file mode 100644 index 00000000..2306eed4 --- /dev/null +++ b/modules/local/spoQC/ambient/main.nf @@ -0,0 +1,54 @@ + +process SPOQC_AMBIENT { + tag "$meta.id" + label 'process_high_cpus' + label 'process_xl_mem' + label 'process_mid_time' + label 'spoqc' + + container "heylf/spoqc:0.0.1" + + input: + tuple val(meta), path(spatialdata, stageAs: "*") + val(step) + + output: + tuple val(meta), path("./report/ambientqc") , emit: report + tuple val(meta), path("./spoQC_tmp/ambient_output_genes.parquet") , emit: tmp + tuple val("${task.process}"), val('spoqc'), eval("spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown"), topic: versions, emit: versions_spoqc + + 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("SPOQC_AMBIENT module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + def args = task.ext.args ?: '' + + """ + python3 -m spoqc \\ + -i ${spatialdata} \\ + -o ./ \\ + -t ./spoQC_tmp/ \\ + -n ${task.cpus} \\ + -s ${step} \\ + --dataset ${meta.id} \\ + ${args} + """ + + stub: + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error("SPOQC_AMBIENT module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + """ + mkdir -p ./report/ambientqc + mkdir -p ./spoQC_tmp + touch ./spoQC_tmp/ambient_output_genes.parquet + """ + +} diff --git a/modules/local/spoQC/ambient/meta.yml b/modules/local/spoQC/ambient/meta.yml new file mode 100644 index 00000000..49221b14 --- /dev/null +++ b/modules/local/spoQC/ambient/meta.yml @@ -0,0 +1,77 @@ +name: spoqc_ambient +description: | + Run ambient-RNA (background gene) QC checks on a SpatialData object. +keywords: + - spatial transcriptomics + - quality control + - spoqc + - xenium + - ambient rna +tools: + - "spoqc": + description: | + spoQC is a modular framework for multimodal quality control (QC) of imaging-based spatially resolved transcriptomics (SRT). + homepage: https://github.com/heylf/spoQC + documentation: https://github.com/heylf/spoQC + licence: ["MIT license"] +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - spatialdata: + type: directory + description: Path to the SpatialData zarr store to run QC on + ontologies: [] + - step: + type: string + description: Name of the spoQC step to run, passed to `spoqc -s` +output: + report: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "report": + type: directory + description: HTML/plot QC report directory produced for this step + ontologies: [] + tmp: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "ambient_output_genes.parquet": + type: file + description: | + Per-gene ambient-RNA QC parquet output, consumed by the HQTR ambient-contamination (hqtr_ac) step. + pattern: "ambient_output_genes.parquet" + ontologies: [] + versions_spoqc: + - - "${task.process}": + type: string + description: The process the versions were collected from + - spoqc: + type: string + description: The tool name + - "spoqc --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 + - spoqc: + type: string + description: The tool name + - "spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown": + type: eval + description: The expression to obtain the version of the tool +authors: + - "@heylf" +maintainers: + - "@heylf" diff --git a/modules/local/spoQC/ambient/tests/main.nf.test b/modules/local/spoQC/ambient/tests/main.nf.test new file mode 100644 index 00000000..081a98c4 --- /dev/null +++ b/modules/local/spoQC/ambient/tests/main.nf.test @@ -0,0 +1,82 @@ +nextflow_process { + + name "Test Process SPOQC_AMBIENT" + script "../main.nf" + process "SPOQC_AMBIENT" + config "./nextflow.config" + + tag "modules" + tag "modules_local" + tag "spoqc" + tag "spoqc/ambient" + + setup { + run("UNTAR") { + script "modules/nf-core/untar/main.nf" + process { + """ + input[0] = [[], file('https://raw.githubusercontent.com/nf-core/test-datasets/spatialaxe/xenium_bundle.tar.gz', checkIfExists: true)] + """ + } + } + + run("SPATIALDATA_WRITE") { + script "modules/local/spatialdata/write/main.nf" + process { + """ + input[0] = channel.of([ + [id: "test_run_spoqc"], + ]).combine(UNTAR.out.untar.map { it[1] }) + input[1] = "spatialdata_raw" + input[2] = [] + input[3] = "all" + """ + } + } + } + + test("spoqc ambient") { + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "ambientqc" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert file(process.out.tmp[0][1]).exists() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + + test("spoqc ambient stub") { + + options "-stub" + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "ambientqc" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert file(process.out.tmp[0][1]).exists() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + +} diff --git a/modules/local/spoQC/ambient/tests/main.nf.test.snap b/modules/local/spoQC/ambient/tests/main.nf.test.snap new file mode 100644 index 00000000..fa9ebb09 --- /dev/null +++ b/modules/local/spoQC/ambient/tests/main.nf.test.snap @@ -0,0 +1,34 @@ +{ + "spoqc ambient": { + "content": [ + [ + [ + "SPOQC_AMBIENT", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-09T16:35:14.607143675", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + }, + "spoqc ambient stub": { + "content": [ + [ + [ + "SPOQC_AMBIENT", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-09T16:35:43.732884944", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + } +} \ No newline at end of file diff --git a/modules/local/spoQC/ambient/tests/nextflow.config b/modules/local/spoQC/ambient/tests/nextflow.config new file mode 100644 index 00000000..f8b3a30a --- /dev/null +++ b/modules/local/spoQC/ambient/tests/nextflow.config @@ -0,0 +1,9 @@ +process { + + resourceLimits = [ + cpus: 4, + memory: '8.GB', + time: '2.h', + ] + +} diff --git a/modules/local/spoQC/analysis_category/main.nf b/modules/local/spoQC/analysis_category/main.nf new file mode 100644 index 00000000..d3140926 --- /dev/null +++ b/modules/local/spoQC/analysis_category/main.nf @@ -0,0 +1,68 @@ +process SPOQC_ANALYSIS_CATEGORY { + tag "${meta.id}" + label 'process_high_cpus' + label 'process_xl_mem' + label 'process_mid_time' + label 'spoqc' + + + container "heylf/spoqc:0.0.1" + + input: + tuple val(meta), path(spatialdata, stageAs: "*") + path(annotation, stageAs: "*") + val(step) + path(tmp_general, stageAs: "spoQC_tmp/generalqc_output_hqcr.parquet") + path(tmp_bubble, stageAs: "spoQC_tmp/bubbleqc_output_hqcr.parquet") + path(tmp_doublet, stageAs: "spoQC_tmp/doubletqc_output_hqcr.parquet") + path(tmp_void, stageAs: "spoQC_tmp/voidqc_output_hqcr.parquet") + path(tmp_cell, stageAs: "spoQC_tmp/cellqc_output_hqcr.parquet") + path(mask_hqcr, stageAs: "spoQC_tmp/hqcr_output_mask_raw.parquet") + path(mask_smoothed_hqcr, stageAs: "spoQC_tmp/hqcr_output_mask_smoothed_raw.parquet") + path(qv, stageAs: "spoQC_tmp/hqtr_output_qv_prob") + path(ac, stageAs: "spoQC_tmp/hqtr_output_ac_prob") + path(metrices_hqtr, stageAs: "spoQC_tmp/metrices/hqtr") + path(mask_smoothed_hqtr, stageAs: "spoQC_tmp/hqtr_output_mask_smoothed_raw") + path(mask_hqtr, stageAs: "spoQC_tmp/hqtr_output_mask_raw") + path(metrices_hqpr, stageAs: "spoQC_tmp/metrices/hqpr/*") + path(mask_smoothed_hqpr, stageAs: "spoQC_tmp/*") + path(mask_hqpr, stageAs: "spoQC_tmp/*") + + output: + tuple val(meta), path("report/analysis/category") , emit: report + tuple val("${task.process}"), val('spoqc'), eval("spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown"), topic: versions, emit: versions_spoqc + + 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("SPOQC_ANALYSIS_CATEGORY module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + def args = task.ext.args ?: '' + def annotation_arg = annotation ? "-a ${annotation}" : "" + + """ + python3 -m spoqc \\ + -i ${spatialdata} \\ + -o ./ \\ + -t ./spoQC_tmp/ \\ + -n ${task.cpus} \\ + ${annotation_arg} \\ + -s ${step} \\ + --dataset ${meta.id} \\ + ${args} + """ + + stub: + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error("SPOQC_ANALYSIS_CATEGORY module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + """ + mkdir -p report/analysis/category + """ +} diff --git a/modules/local/spoQC/analysis_category/meta.yml b/modules/local/spoQC/analysis_category/meta.yml new file mode 100644 index 00000000..3276a501 --- /dev/null +++ b/modules/local/spoQC/analysis_category/meta.yml @@ -0,0 +1,147 @@ +name: spoqc_analysis_category +description: | + Generate a downstream per-category QC analysis report, combining all upstream spoQC outputs. +keywords: + - spatial transcriptomics + - quality control + - spoqc + - xenium + - downstream analysis +tools: + - "spoqc": + description: | + spoQC is a modular framework for multimodal quality control (QC) of imaging-based spatially resolved transcriptomics (SRT). + homepage: https://github.com/heylf/spoQC + documentation: https://github.com/heylf/spoQC + licence: ["MIT license"] +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - spatialdata: + type: directory + description: Path to the SpatialData zarr store to run QC on + ontologies: [] + - annotation: + type: file + description: | + Optional cell-type annotation file. When empty/absent, spoqc is run without the `-a` flag. + pattern: "*.tsv" + ontologies: [] + - step: + type: string + description: Name of the spoQC step to run, passed to `spoqc -s` + - tmp_general: + type: file + description: General QC parquet output from SPOQC_GENERAL + pattern: "generalqc_output_hqcr.parquet" + ontologies: [] + - tmp_bubble: + type: file + description: Bubble QC parquet output from SPOQC_BUBBLE + pattern: "bubbleqc_output_hqcr.parquet" + ontologies: [] + - tmp_doublet: + type: file + description: Doublet QC parquet output from SPOQC_DOUBLET + pattern: "doubletqc_output_hqcr.parquet" + ontologies: [] + - tmp_void: + type: file + description: Void QC parquet output from SPOQC_VOID + pattern: "voidqc_output_hqcr.parquet" + ontologies: [] + - tmp_cell: + type: file + description: Cell QC parquet output from SPOQC_CELL + pattern: "cellqc_output_hqcr.parquet" + ontologies: [] + - mask_hqcr: + type: file + description: HQCR raw mask from SPOQC_HQCR_IDENT + pattern: "hqcr_output_mask_raw.parquet" + ontologies: [] + - mask_smoothed_hqcr: + type: file + description: Smoothed HQCR mask from SPOQC_HQCR_IDENT + pattern: "hqcr_output_mask_smoothed_raw.parquet" + ontologies: [] + - qv: + type: directory + description: HQTR quality-value probability output from SPOQC_HQTR_QV + pattern: "hqtr_output_qv_prob" + ontologies: [] + - ac: + type: directory + description: HQTR ambient-contamination probability output from SPOQC_HQTR_AC + pattern: "hqtr_output_ac_prob" + ontologies: [] + - metrices_hqtr: + type: directory + description: HQTR metrics directory from SPOQC_HQTR_METRICES + pattern: "spoQC_tmp/metrices/hqtr" + ontologies: [] + - mask_smoothed_hqtr: + type: directory + description: Smoothed HQTR mask from SPOQC_HQTR_REFINEMENT + pattern: "hqtr_output_mask_smoothed_raw" + ontologies: [] + - mask_hqtr: + type: directory + description: HQTR raw mask from SPOQC_HQTR_CLUSTERING + pattern: "hqtr_output_mask_raw" + ontologies: [] + - metrices_hqpr: + type: directory + description: Collected per-staining HQPR metrics directories from SPOQC_HQPR_METRICES + pattern: "spoQC_tmp/metrices/hqpr/*" + ontologies: [] + - mask_smoothed_hqpr: + type: directory + description: Collected per-staining smoothed HQPR masks from SPOQC_HQPR_REFINEMENT + pattern: "spoQC_tmp/*" + ontologies: [] + - mask_hqpr: + type: directory + description: Collected per-staining raw HQPR masks from SPOQC_HQPR_CLUSTERING + pattern: "spoQC_tmp/*" + ontologies: [] +output: + report: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "report/analysis/category": + type: directory + description: HTML/plot per-category analysis report directory + pattern: "report/analysis/category" + ontologies: [] + versions_spoqc: + - - "${task.process}": + type: string + description: The process the versions were collected from + - spoqc: + type: string + description: The tool name + - "spoqc --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 + - spoqc: + type: string + description: The tool name + - "spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown": + type: eval + description: The expression to obtain the version of the tool +authors: + - "@heylf" +maintainers: + - "@heylf" diff --git a/modules/local/spoQC/analysis_category/tests/main.nf.test b/modules/local/spoQC/analysis_category/tests/main.nf.test new file mode 100644 index 00000000..e2601fa6 --- /dev/null +++ b/modules/local/spoQC/analysis_category/tests/main.nf.test @@ -0,0 +1,287 @@ +nextflow_process { + + name "Test Process SPOQC_ANALYSIS_CATEGORY" + script "../main.nf" + process "SPOQC_ANALYSIS_CATEGORY" + config "./nextflow.config" + + tag "modules" + tag "modules_local" + tag "spoqc" + tag "spoqc/analysis_category" + + setup { + run("UNTAR") { + script "modules/nf-core/untar/main.nf" + process { + """ + input[0] = [[], file('https://raw.githubusercontent.com/nf-core/test-datasets/spatialaxe/xenium_bundle.tar.gz', checkIfExists: true)] + """ + } + } + + run("SPATIALDATA_WRITE") { + script "modules/local/spatialdata/write/main.nf" + process { + """ + input[0] = channel.of([ + [id: "test_run_spoqc"], + ]).combine(UNTAR.out.untar.map { it[1] }) + input[1] = "spatialdata_raw" + input[2] = [] + input[3] = "all" + """ + } + } + + run("SPOQC_ANNOTATION") { + script "modules/local/spoQC/annotation/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = [] + input[2] = "annotation" + """ + } + } + + run("SPOQC_GENERAL") { + script "modules/local/spoQC/general/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "generalqc" + """ + } + } + + run("SPOQC_BUBBLE") { + script "modules/local/spoQC/bubble/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "bubbleqc" + """ + } + } + + run("SPOQC_DOUBLET") { + script "modules/local/spoQC/doublet/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "doubletqc" + """ + } + } + + run("SPOQC_VOID") { + script "modules/local/spoQC/void/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "voidqc" + """ + } + } + + run("SPOQC_CELL") { + script "modules/local/spoQC/cell/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "cellqc" + """ + } + } + + run("SPOQC_HQCR_IDENT") { + script "modules/local/spoQC/hqcr_ident/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqcr_ident" + input[2] = SPOQC_GENERAL.out.tmp.map { it[1] } + input[3] = SPOQC_BUBBLE.out.tmp.map { it[1] } + input[4] = SPOQC_DOUBLET.out.tmp.map { it[1] } + input[5] = SPOQC_VOID.out.tmp.map { it[1] } + input[6] = SPOQC_CELL.out.tmp.map { it[1] } + """ + } + } + + run("SPOQC_AMBIENT") { + script "modules/local/spoQC/ambient/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "ambientqc" + """ + } + } + + run("SPOQC_HQTR_METRICES") { + script "modules/local/spoQC/hqtr_metrices/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_metrices" + """ + } + } + + run("SPOQC_HQTR_QV") { + script "modules/local/spoQC/hqtr_qv/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_qv" + """ + } + } + + run("SPOQC_HQTR_AC") { + script "modules/local/spoQC/hqtr_ac/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_ac" + input[2] = SPOQC_AMBIENT.out.tmp.map { it[1] } + """ + } + } + + run("SPOQC_HQTR_CLUSTERING") { + script "modules/local/spoQC/hqtr_clustering/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_clustering" + input[2] = SPOQC_HQTR_METRICES.out.metrices.map { it[1] } + input[3] = SPOQC_HQTR_QV.out.tmp.map { it[1] } + input[4] = SPOQC_HQTR_AC.out.tmp.map { it[1] } + """ + } + } + + run("SPOQC_HQTR_REFINEMENT") { + script "modules/local/spoQC/hqtr_refinement/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_refinement" + input[2] = SPOQC_HQTR_CLUSTERING.out.mask.map { it[1] } + """ + } + } + + run("SPOQC_HQPR_METRICES") { + script "modules/local/spoQC/hqpr_metrices/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata.combine(channel.of(0)) + input[1] = "hqpr_metrices" + """ + } + } + + run("SPOQC_HQPR_CLUSTERING") { + script "modules/local/spoQC/hqpr_clustering/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata.combine(channel.of(0)) + input[1] = "hqpr_clustering" + input[2] = SPOQC_HQPR_METRICES.out.metrices.map { _meta, staining, f -> [staining, f] } + """ + } + } + + run("SPOQC_HQPR_REFINEMENT") { + script "modules/local/spoQC/hqpr_refinement/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata.combine(channel.of(0)) + input[1] = "hqpr_refinement" + input[2] = SPOQC_HQPR_CLUSTERING.out.mask.map { _meta, staining, f -> [staining, f] } + """ + } + } + } + + test("spoqc analysis_category") { + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "analysis_category" + input[3] = SPOQC_GENERAL.out.tmp.map { it[1] } + input[4] = SPOQC_BUBBLE.out.tmp.map { it[1] } + input[5] = SPOQC_DOUBLET.out.tmp.map { it[1] } + input[6] = SPOQC_VOID.out.tmp.map { it[1] } + input[7] = SPOQC_CELL.out.tmp.map { it[1] } + input[8] = SPOQC_HQCR_IDENT.out.mask.map { it[1] } + input[9] = SPOQC_HQCR_IDENT.out.mask_smoothed.map { it[1] } + input[10] = SPOQC_HQTR_QV.out.tmp.map { it[1] } + input[11] = SPOQC_HQTR_AC.out.tmp.map { it[1] } + input[12] = SPOQC_HQTR_METRICES.out.metrices.map { it[1] } + input[13] = SPOQC_HQTR_REFINEMENT.out.mask_smoothed.map { it[1] } + input[14] = SPOQC_HQTR_CLUSTERING.out.mask.map { it[1] } + input[15] = SPOQC_HQPR_METRICES.out.metrices.map { _meta, _staining, p -> p }.collect() + input[16] = SPOQC_HQPR_REFINEMENT.out.mask_smoothed.map { _meta, _staining, p -> p }.collect() + input[17] = SPOQC_HQPR_CLUSTERING.out.mask.map { _meta, _staining, p -> p }.collect() + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + + test("spoqc analysis_category stub") { + + options "-stub" + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "analysis_category" + input[3] = SPOQC_GENERAL.out.tmp.map { it[1] } + input[4] = SPOQC_BUBBLE.out.tmp.map { it[1] } + input[5] = SPOQC_DOUBLET.out.tmp.map { it[1] } + input[6] = SPOQC_VOID.out.tmp.map { it[1] } + input[7] = SPOQC_CELL.out.tmp.map { it[1] } + input[8] = SPOQC_HQCR_IDENT.out.mask.map { it[1] } + input[9] = SPOQC_HQCR_IDENT.out.mask_smoothed.map { it[1] } + input[10] = SPOQC_HQTR_QV.out.tmp.map { it[1] } + input[11] = SPOQC_HQTR_AC.out.tmp.map { it[1] } + input[12] = SPOQC_HQTR_METRICES.out.metrices.map { it[1] } + input[13] = SPOQC_HQTR_REFINEMENT.out.mask_smoothed.map { it[1] } + input[14] = SPOQC_HQTR_CLUSTERING.out.mask.map { it[1] } + input[15] = SPOQC_HQPR_METRICES.out.metrices.map { _meta, _staining, p -> p }.collect() + input[16] = SPOQC_HQPR_REFINEMENT.out.mask_smoothed.map { _meta, _staining, p -> p }.collect() + input[17] = SPOQC_HQPR_CLUSTERING.out.mask.map { _meta, _staining, p -> p }.collect() + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + +} diff --git a/modules/local/spoQC/analysis_category/tests/main.nf.test.snap b/modules/local/spoQC/analysis_category/tests/main.nf.test.snap new file mode 100644 index 00000000..c22738c2 --- /dev/null +++ b/modules/local/spoQC/analysis_category/tests/main.nf.test.snap @@ -0,0 +1,34 @@ +{ + "spoqc analysis_category stub": { + "content": [ + [ + [ + "SPOQC_ANALYSIS_CATEGORY", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-10T10:42:41.614123792", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + }, + "spoqc analysis_category": { + "content": [ + [ + [ + "SPOQC_ANALYSIS_CATEGORY", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-10T10:36:48.193373778", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + } +} \ No newline at end of file diff --git a/modules/local/spoQC/analysis_category/tests/nextflow.config b/modules/local/spoQC/analysis_category/tests/nextflow.config new file mode 100644 index 00000000..a39d075a --- /dev/null +++ b/modules/local/spoQC/analysis_category/tests/nextflow.config @@ -0,0 +1,9 @@ +process { + + resourceLimits = [ + cpus: 4, + memory: '14.GB', + time: '2.h', + ] + +} diff --git a/modules/local/spoQC/analysis_cluster/main.nf b/modules/local/spoQC/analysis_cluster/main.nf new file mode 100644 index 00000000..c2344507 --- /dev/null +++ b/modules/local/spoQC/analysis_cluster/main.nf @@ -0,0 +1,70 @@ +process SPOQC_ANALYSIS_CLUSTER { + tag "${meta.id}" + label 'process_high_cpus' + label 'process_xl_mem' + label 'process_mid_time' + label 'spoqc' + + + container "heylf/spoqc:0.0.1" + + input: + tuple val(meta), path(spatialdata, stageAs: "*") + path(annotation, stageAs: "*") + val(step) + path(tmp_general, stageAs: "spoQC_tmp/generalqc_output_hqcr.parquet") + path(tmp_bubble, stageAs: "spoQC_tmp/bubbleqc_output_hqcr.parquet") + path(tmp_doublet, stageAs: "spoQC_tmp/doubletqc_output_hqcr.parquet") + path(tmp_void, stageAs: "spoQC_tmp/voidqc_output_hqcr.parquet") + path(tmp_cell, stageAs: "spoQC_tmp/cellqc_output_hqcr.parquet") + path(mask_hqcr, stageAs: "spoQC_tmp/hqcr_output_mask_raw.parquet") + path(mask_smoothed_hqcr, stageAs: "spoQC_tmp/hqcr_output_mask_smoothed_raw.parquet") + path(qv, stageAs: "spoQC_tmp/hqtr_output_qv_prob") + path(ac, stageAs: "spoQC_tmp/hqtr_output_ac_prob") + path(metrices_hqtr, stageAs: "spoQC_tmp/metrices/hqtr") + path(mask_smoothed_hqtr, stageAs: "spoQC_tmp/hqtr_output_mask_smoothed_raw") + path(mask_hqtr, stageAs: "spoQC_tmp/hqtr_output_mask_raw") + path(metrices_hqpr, stageAs: "spoQC_tmp/metrices/hqpr/*") + path(mask_smoothed_hqpr, stageAs: "spoQC_tmp/*") + path(mask_hqpr, stageAs: "spoQC_tmp/*") + + output: + tuple val(meta), path("report/analysis/cluster") , emit: report + tuple val(meta), path("report/analysis/rna_cluster.h5ad") , emit: h5ad + tuple val("${task.process}"), val('spoqc'), eval("spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown"), topic: versions, emit: versions_spoqc + + 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("SPOQC_ANALYSIS_CLUSTER module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + def args = task.ext.args ?: '' + def annotation_arg = annotation ? "-a ${annotation}" : "" + + """ + python3 -m spoqc \\ + -i ${spatialdata} \\ + -o ./ \\ + -t ./spoQC_tmp/ \\ + -n ${task.cpus} \\ + ${annotation_arg} \\ + -s ${step} \\ + --dataset ${meta.id} \\ + ${args} + """ + + stub: + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error("SPOQC_ANALYSIS_CLUSTER module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + """ + mkdir -p report/analysis/cluster + touch report/analysis/rna_cluster.h5ad + """ +} diff --git a/modules/local/spoQC/analysis_cluster/meta.yml b/modules/local/spoQC/analysis_cluster/meta.yml new file mode 100644 index 00000000..1ab68b9d --- /dev/null +++ b/modules/local/spoQC/analysis_cluster/meta.yml @@ -0,0 +1,160 @@ +name: spoqc_analysis_cluster +description: | + Generate the downstream clustering QC analysis report and a clustered AnnData object, combining all upstream spoQC outputs. +keywords: + - spatial transcriptomics + - quality control + - spoqc + - xenium + - downstream analysis + - clustering + - anndata +tools: + - "spoqc": + description: | + spoQC is a modular framework for multimodal quality control (QC) of imaging-based spatially resolved transcriptomics (SRT). + homepage: https://github.com/heylf/spoQC + documentation: https://github.com/heylf/spoQC + licence: ["MIT license"] +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - spatialdata: + type: directory + description: Path to the SpatialData zarr store to run QC on + ontologies: [] + - annotation: + type: file + description: | + Optional cell-type annotation file. When empty/absent, spoqc is run without the `-a` flag. + pattern: "*.tsv" + ontologies: [] + - step: + type: string + description: Name of the spoQC step to run, passed to `spoqc -s` + - tmp_general: + type: file + description: General QC parquet output from SPOQC_GENERAL + pattern: "generalqc_output_hqcr.parquet" + ontologies: [] + - tmp_bubble: + type: file + description: Bubble QC parquet output from SPOQC_BUBBLE + pattern: "bubbleqc_output_hqcr.parquet" + ontologies: [] + - tmp_doublet: + type: file + description: Doublet QC parquet output from SPOQC_DOUBLET + pattern: "doubletqc_output_hqcr.parquet" + ontologies: [] + - tmp_void: + type: file + description: Void QC parquet output from SPOQC_VOID + pattern: "voidqc_output_hqcr.parquet" + ontologies: [] + - tmp_cell: + type: file + description: Cell QC parquet output from SPOQC_CELL + pattern: "cellqc_output_hqcr.parquet" + ontologies: [] + - mask_hqcr: + type: file + description: HQCR raw mask from SPOQC_HQCR_IDENT + pattern: "hqcr_output_mask_raw.parquet" + ontologies: [] + - mask_smoothed_hqcr: + type: file + description: Smoothed HQCR mask from SPOQC_HQCR_IDENT + pattern: "hqcr_output_mask_smoothed_raw.parquet" + ontologies: [] + - qv: + type: directory + description: HQTR quality-value probability output from SPOQC_HQTR_QV + pattern: "hqtr_output_qv_prob" + ontologies: [] + - ac: + type: directory + description: HQTR ambient-contamination probability output from SPOQC_HQTR_AC + pattern: "hqtr_output_ac_prob" + ontologies: [] + - metrices_hqtr: + type: directory + description: HQTR metrics directory from SPOQC_HQTR_METRICES + pattern: "spoQC_tmp/metrices/hqtr" + ontologies: [] + - mask_smoothed_hqtr: + type: directory + description: Smoothed HQTR mask from SPOQC_HQTR_REFINEMENT + pattern: "hqtr_output_mask_smoothed_raw" + ontologies: [] + - mask_hqtr: + type: directory + description: HQTR raw mask from SPOQC_HQTR_CLUSTERING + pattern: "hqtr_output_mask_raw" + ontologies: [] + - metrices_hqpr: + type: directory + description: Collected per-staining HQPR metrics directories from SPOQC_HQPR_METRICES + pattern: "spoQC_tmp/metrices/hqpr/*" + ontologies: [] + - mask_smoothed_hqpr: + type: directory + description: Collected per-staining smoothed HQPR masks from SPOQC_HQPR_REFINEMENT + pattern: "spoQC_tmp/*" + ontologies: [] + - mask_hqpr: + type: directory + description: Collected per-staining raw HQPR masks from SPOQC_HQPR_CLUSTERING + pattern: "spoQC_tmp/*" + ontologies: [] +output: + report: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "report/analysis/cluster": + type: directory + description: HTML/plot clustering analysis report directory + pattern: "report/analysis/cluster" + ontologies: [] + h5ad: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "report/analysis/rna_cluster.h5ad": + type: file + description: Clustered AnnData object combining all spoQC outputs + pattern: "report/analysis/rna_cluster.h5ad" + ontologies: [] + versions_spoqc: + - - "${task.process}": + type: string + description: The process the versions were collected from + - spoqc: + type: string + description: The tool name + - "spoqc --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 + - spoqc: + type: string + description: The tool name + - "spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown": + type: eval + description: The expression to obtain the version of the tool +authors: + - "@heylf" +maintainers: + - "@heylf" diff --git a/modules/local/spoQC/analysis_cluster/tests/main.nf.test b/modules/local/spoQC/analysis_cluster/tests/main.nf.test new file mode 100644 index 00000000..79d7cf34 --- /dev/null +++ b/modules/local/spoQC/analysis_cluster/tests/main.nf.test @@ -0,0 +1,289 @@ +nextflow_process { + + name "Test Process SPOQC_ANALYSIS_CLUSTER" + script "../main.nf" + process "SPOQC_ANALYSIS_CLUSTER" + config "./nextflow.config" + + tag "modules" + tag "modules_local" + tag "spoqc" + tag "spoqc/analysis_cluster" + + setup { + run("UNTAR") { + script "modules/nf-core/untar/main.nf" + process { + """ + input[0] = [[], file('https://raw.githubusercontent.com/nf-core/test-datasets/spatialaxe/xenium_bundle.tar.gz', checkIfExists: true)] + """ + } + } + + run("SPATIALDATA_WRITE") { + script "modules/local/spatialdata/write/main.nf" + process { + """ + input[0] = channel.of([ + [id: "test_run_spoqc"], + ]).combine(UNTAR.out.untar.map { it[1] }) + input[1] = "spatialdata_raw" + input[2] = [] + input[3] = "all" + """ + } + } + + run("SPOQC_ANNOTATION") { + script "modules/local/spoQC/annotation/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = [] + input[2] = "annotation" + """ + } + } + + run("SPOQC_GENERAL") { + script "modules/local/spoQC/general/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "generalqc" + """ + } + } + + run("SPOQC_BUBBLE") { + script "modules/local/spoQC/bubble/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "bubbleqc" + """ + } + } + + run("SPOQC_DOUBLET") { + script "modules/local/spoQC/doublet/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "doubletqc" + """ + } + } + + run("SPOQC_VOID") { + script "modules/local/spoQC/void/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "voidqc" + """ + } + } + + run("SPOQC_CELL") { + script "modules/local/spoQC/cell/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "cellqc" + """ + } + } + + run("SPOQC_HQCR_IDENT") { + script "modules/local/spoQC/hqcr_ident/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqcr_ident" + input[2] = SPOQC_GENERAL.out.tmp.map { it[1] } + input[3] = SPOQC_BUBBLE.out.tmp.map { it[1] } + input[4] = SPOQC_DOUBLET.out.tmp.map { it[1] } + input[5] = SPOQC_VOID.out.tmp.map { it[1] } + input[6] = SPOQC_CELL.out.tmp.map { it[1] } + """ + } + } + + run("SPOQC_AMBIENT") { + script "modules/local/spoQC/ambient/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "ambientqc" + """ + } + } + + run("SPOQC_HQTR_METRICES") { + script "modules/local/spoQC/hqtr_metrices/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_metrices" + """ + } + } + + run("SPOQC_HQTR_QV") { + script "modules/local/spoQC/hqtr_qv/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_qv" + """ + } + } + + run("SPOQC_HQTR_AC") { + script "modules/local/spoQC/hqtr_ac/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_ac" + input[2] = SPOQC_AMBIENT.out.tmp.map { it[1] } + """ + } + } + + run("SPOQC_HQTR_CLUSTERING") { + script "modules/local/spoQC/hqtr_clustering/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_clustering" + input[2] = SPOQC_HQTR_METRICES.out.metrices.map { it[1] } + input[3] = SPOQC_HQTR_QV.out.tmp.map { it[1] } + input[4] = SPOQC_HQTR_AC.out.tmp.map { it[1] } + """ + } + } + + run("SPOQC_HQTR_REFINEMENT") { + script "modules/local/spoQC/hqtr_refinement/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_refinement" + input[2] = SPOQC_HQTR_CLUSTERING.out.mask.map { it[1] } + """ + } + } + + run("SPOQC_HQPR_METRICES") { + script "modules/local/spoQC/hqpr_metrices/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata.combine(channel.of(0)) + input[1] = "hqpr_metrices" + """ + } + } + + run("SPOQC_HQPR_CLUSTERING") { + script "modules/local/spoQC/hqpr_clustering/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata.combine(channel.of(0)) + input[1] = "hqpr_clustering" + input[2] = SPOQC_HQPR_METRICES.out.metrices.map { _meta, staining, f -> [staining, f] } + """ + } + } + + run("SPOQC_HQPR_REFINEMENT") { + script "modules/local/spoQC/hqpr_refinement/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata.combine(channel.of(0)) + input[1] = "hqpr_refinement" + input[2] = SPOQC_HQPR_CLUSTERING.out.mask.map { _meta, staining, f -> [staining, f] } + """ + } + } + } + + test("spoqc analysis_cluster") { + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "analysis_cluster" + input[3] = SPOQC_GENERAL.out.tmp.map { it[1] } + input[4] = SPOQC_BUBBLE.out.tmp.map { it[1] } + input[5] = SPOQC_DOUBLET.out.tmp.map { it[1] } + input[6] = SPOQC_VOID.out.tmp.map { it[1] } + input[7] = SPOQC_CELL.out.tmp.map { it[1] } + input[8] = SPOQC_HQCR_IDENT.out.mask.map { it[1] } + input[9] = SPOQC_HQCR_IDENT.out.mask_smoothed.map { it[1] } + input[10] = SPOQC_HQTR_QV.out.tmp.map { it[1] } + input[11] = SPOQC_HQTR_AC.out.tmp.map { it[1] } + input[12] = SPOQC_HQTR_METRICES.out.metrices.map { it[1] } + input[13] = SPOQC_HQTR_REFINEMENT.out.mask_smoothed.map { it[1] } + input[14] = SPOQC_HQTR_CLUSTERING.out.mask.map { it[1] } + input[15] = SPOQC_HQPR_METRICES.out.metrices.map { _meta, _staining, p -> p }.collect() + input[16] = SPOQC_HQPR_REFINEMENT.out.mask_smoothed.map { _meta, _staining, p -> p }.collect() + input[17] = SPOQC_HQPR_CLUSTERING.out.mask.map { _meta, _staining, p -> p }.collect() + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert file(process.out.h5ad[0][1]).exists() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + + test("spoqc analysis_cluster stub") { + + options "-stub" + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "analysis_cluster" + input[3] = SPOQC_GENERAL.out.tmp.map { it[1] } + input[4] = SPOQC_BUBBLE.out.tmp.map { it[1] } + input[5] = SPOQC_DOUBLET.out.tmp.map { it[1] } + input[6] = SPOQC_VOID.out.tmp.map { it[1] } + input[7] = SPOQC_CELL.out.tmp.map { it[1] } + input[8] = SPOQC_HQCR_IDENT.out.mask.map { it[1] } + input[9] = SPOQC_HQCR_IDENT.out.mask_smoothed.map { it[1] } + input[10] = SPOQC_HQTR_QV.out.tmp.map { it[1] } + input[11] = SPOQC_HQTR_AC.out.tmp.map { it[1] } + input[12] = SPOQC_HQTR_METRICES.out.metrices.map { it[1] } + input[13] = SPOQC_HQTR_REFINEMENT.out.mask_smoothed.map { it[1] } + input[14] = SPOQC_HQTR_CLUSTERING.out.mask.map { it[1] } + input[15] = SPOQC_HQPR_METRICES.out.metrices.map { _meta, _staining, p -> p }.collect() + input[16] = SPOQC_HQPR_REFINEMENT.out.mask_smoothed.map { _meta, _staining, p -> p }.collect() + input[17] = SPOQC_HQPR_CLUSTERING.out.mask.map { _meta, _staining, p -> p }.collect() + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert file(process.out.h5ad[0][1]).exists() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + +} diff --git a/modules/local/spoQC/analysis_cluster/tests/main.nf.test.snap b/modules/local/spoQC/analysis_cluster/tests/main.nf.test.snap new file mode 100644 index 00000000..22a9af33 --- /dev/null +++ b/modules/local/spoQC/analysis_cluster/tests/main.nf.test.snap @@ -0,0 +1,34 @@ +{ + "spoqc analysis_cluster stub": { + "content": [ + [ + [ + "SPOQC_ANALYSIS_CLUSTER", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T14:15:16.133977021", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + }, + "spoqc analysis_cluster": { + "content": [ + [ + [ + "SPOQC_ANALYSIS_CLUSTER", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T14:09:17.966837606", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + } +} \ No newline at end of file diff --git a/modules/local/spoQC/analysis_cluster/tests/nextflow.config b/modules/local/spoQC/analysis_cluster/tests/nextflow.config new file mode 100644 index 00000000..a39d075a --- /dev/null +++ b/modules/local/spoQC/analysis_cluster/tests/nextflow.config @@ -0,0 +1,9 @@ +process { + + resourceLimits = [ + cpus: 4, + memory: '14.GB', + time: '2.h', + ] + +} diff --git a/modules/local/spoQC/analysis_overview/main.nf b/modules/local/spoQC/analysis_overview/main.nf new file mode 100644 index 00000000..bd0d37a7 --- /dev/null +++ b/modules/local/spoQC/analysis_overview/main.nf @@ -0,0 +1,70 @@ +process SPOQC_ANALYSIS_OVERVIEW { + tag "${meta.id}" + label 'process_high_cpus' + label 'process_xl_mem' + label 'process_mid_time' + label 'spoqc' + + + container "heylf/spoqc:0.0.1" + + input: + tuple val(meta), path(spatialdata, stageAs: "*") + path(annotation, stageAs: "*") + val(step) + path(tmp_general, stageAs: "spoQC_tmp/generalqc_output_hqcr.parquet") + path(tmp_bubble, stageAs: "spoQC_tmp/bubbleqc_output_hqcr.parquet") + path(tmp_doublet, stageAs: "spoQC_tmp/doubletqc_output_hqcr.parquet") + path(tmp_void, stageAs: "spoQC_tmp/voidqc_output_hqcr.parquet") + path(tmp_cell, stageAs: "spoQC_tmp/cellqc_output_hqcr.parquet") + path(mask_hqcr, stageAs: "spoQC_tmp/hqcr_output_mask_raw.parquet") + path(mask_smoothed_hqcr, stageAs: "spoQC_tmp/hqcr_output_mask_smoothed_raw.parquet") + path(qv, stageAs: "spoQC_tmp/hqtr_output_qv_prob") + path(ac, stageAs: "spoQC_tmp/hqtr_output_ac_prob") + path(metrices_hqtr, stageAs: "spoQC_tmp/metrices/hqtr") + path(mask_smoothed_hqtr, stageAs: "spoQC_tmp/hqtr_output_mask_smoothed_raw") + path(mask_hqtr, stageAs: "spoQC_tmp/hqtr_output_mask_raw") + path(metrices_hqpr, stageAs: "spoQC_tmp/metrices/hqpr/*") + path(mask_smoothed_hqpr, stageAs: "spoQC_tmp/*") + path(mask_hqpr, stageAs: "spoQC_tmp/*") + + output: + tuple val(meta), path("report/analysis/overview") , emit: report + tuple val(meta), path("report/analysis/rna_qc_annotated.h5ad") , emit: h5ad + tuple val("${task.process}"), val('spoqc'), eval("spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown"), topic: versions, emit: versions_spoqc + + 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("SPOQC_ANALYSIS_OVERVIEW module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + def args = task.ext.args ?: '' + def annotation_arg = annotation ? "-a ${annotation}" : "" + + """ + python3 -m spoqc \\ + -i ${spatialdata} \\ + -o ./ \\ + -t ./spoQC_tmp/ \\ + -n ${task.cpus} \\ + ${annotation_arg} \\ + -s ${step} \\ + --dataset ${meta.id} \\ + ${args} + """ + + stub: + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error("SPOQC_ANALYSIS_OVERVIEW module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + """ + mkdir -p report/analysis/overview + touch report/analysis/rna_qc_annotated.h5ad + """ +} diff --git a/modules/local/spoQC/analysis_overview/meta.yml b/modules/local/spoQC/analysis_overview/meta.yml new file mode 100644 index 00000000..e9d436fb --- /dev/null +++ b/modules/local/spoQC/analysis_overview/meta.yml @@ -0,0 +1,159 @@ +name: spoqc_analysis_overview +description: | + Generate the downstream QC analysis overview report and an annotated AnnData object, combining all upstream spoQC outputs. +keywords: + - spatial transcriptomics + - quality control + - spoqc + - xenium + - downstream analysis + - anndata +tools: + - "spoqc": + description: | + spoQC is a modular framework for multimodal quality control (QC) of imaging-based spatially resolved transcriptomics (SRT). + homepage: https://github.com/heylf/spoQC + documentation: https://github.com/heylf/spoQC + licence: ["MIT license"] +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - spatialdata: + type: directory + description: Path to the SpatialData zarr store to run QC on + ontologies: [] + - annotation: + type: file + description: | + Optional cell-type annotation file. When empty/absent, spoqc is run without the `-a` flag. + pattern: "*.tsv" + ontologies: [] + - step: + type: string + description: Name of the spoQC step to run, passed to `spoqc -s` + - tmp_general: + type: file + description: General QC parquet output from SPOQC_GENERAL + pattern: "generalqc_output_hqcr.parquet" + ontologies: [] + - tmp_bubble: + type: file + description: Bubble QC parquet output from SPOQC_BUBBLE + pattern: "bubbleqc_output_hqcr.parquet" + ontologies: [] + - tmp_doublet: + type: file + description: Doublet QC parquet output from SPOQC_DOUBLET + pattern: "doubletqc_output_hqcr.parquet" + ontologies: [] + - tmp_void: + type: file + description: Void QC parquet output from SPOQC_VOID + pattern: "voidqc_output_hqcr.parquet" + ontologies: [] + - tmp_cell: + type: file + description: Cell QC parquet output from SPOQC_CELL + pattern: "cellqc_output_hqcr.parquet" + ontologies: [] + - mask_hqcr: + type: file + description: HQCR raw mask from SPOQC_HQCR_IDENT + pattern: "hqcr_output_mask_raw.parquet" + ontologies: [] + - mask_smoothed_hqcr: + type: file + description: Smoothed HQCR mask from SPOQC_HQCR_IDENT + pattern: "hqcr_output_mask_smoothed_raw.parquet" + ontologies: [] + - qv: + type: directory + description: HQTR quality-value probability output from SPOQC_HQTR_QV + pattern: "hqtr_output_qv_prob" + ontologies: [] + - ac: + type: directory + description: HQTR ambient-contamination probability output from SPOQC_HQTR_AC + pattern: "hqtr_output_ac_prob" + ontologies: [] + - metrices_hqtr: + type: directory + description: HQTR metrics directory from SPOQC_HQTR_METRICES + pattern: "spoQC_tmp/metrices/hqtr" + ontologies: [] + - mask_smoothed_hqtr: + type: directory + description: Smoothed HQTR mask from SPOQC_HQTR_REFINEMENT + pattern: "hqtr_output_mask_smoothed_raw" + ontologies: [] + - mask_hqtr: + type: directory + description: HQTR raw mask from SPOQC_HQTR_CLUSTERING + pattern: "hqtr_output_mask_raw" + ontologies: [] + - metrices_hqpr: + type: directory + description: Collected per-staining HQPR metrics directories from SPOQC_HQPR_METRICES + pattern: "spoQC_tmp/metrices/hqpr/*" + ontologies: [] + - mask_smoothed_hqpr: + type: directory + description: Collected per-staining smoothed HQPR masks from SPOQC_HQPR_REFINEMENT + pattern: "spoQC_tmp/*" + ontologies: [] + - mask_hqpr: + type: directory + description: Collected per-staining raw HQPR masks from SPOQC_HQPR_CLUSTERING + pattern: "spoQC_tmp/*" + ontologies: [] +output: + report: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "report/analysis/overview": + type: directory + description: HTML/plot analysis overview report directory + pattern: "report/analysis/overview" + ontologies: [] + h5ad: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "report/analysis/rna_qc_annotated.h5ad": + type: file + description: Annotated AnnData object combining all spoQC outputs + pattern: "report/analysis/rna_qc_annotated.h5ad" + ontologies: [] + versions_spoqc: + - - "${task.process}": + type: string + description: The process the versions were collected from + - spoqc: + type: string + description: The tool name + - "spoqc --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 + - spoqc: + type: string + description: The tool name + - "spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown": + type: eval + description: The expression to obtain the version of the tool +authors: + - "@heylf" +maintainers: + - "@heylf" diff --git a/modules/local/spoQC/analysis_overview/tests/main.nf.test b/modules/local/spoQC/analysis_overview/tests/main.nf.test new file mode 100644 index 00000000..fe9ecccf --- /dev/null +++ b/modules/local/spoQC/analysis_overview/tests/main.nf.test @@ -0,0 +1,289 @@ +nextflow_process { + + name "Test Process SPOQC_ANALYSIS_OVERVIEW" + script "../main.nf" + process "SPOQC_ANALYSIS_OVERVIEW" + config "./nextflow.config" + + tag "modules" + tag "modules_local" + tag "spoqc" + tag "spoqc/analysis_overview" + + setup { + run("UNTAR") { + script "modules/nf-core/untar/main.nf" + process { + """ + input[0] = [[], file('https://raw.githubusercontent.com/nf-core/test-datasets/spatialaxe/xenium_bundle.tar.gz', checkIfExists: true)] + """ + } + } + + run("SPATIALDATA_WRITE") { + script "modules/local/spatialdata/write/main.nf" + process { + """ + input[0] = channel.of([ + [id: "test_run_spoqc"], + ]).combine(UNTAR.out.untar.map { it[1] }) + input[1] = "spatialdata_raw" + input[2] = [] + input[3] = "all" + """ + } + } + + run("SPOQC_ANNOTATION") { + script "modules/local/spoQC/annotation/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = [] + input[2] = "annotation" + """ + } + } + + run("SPOQC_GENERAL") { + script "modules/local/spoQC/general/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "generalqc" + """ + } + } + + run("SPOQC_BUBBLE") { + script "modules/local/spoQC/bubble/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "bubbleqc" + """ + } + } + + run("SPOQC_DOUBLET") { + script "modules/local/spoQC/doublet/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "doubletqc" + """ + } + } + + run("SPOQC_VOID") { + script "modules/local/spoQC/void/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "voidqc" + """ + } + } + + run("SPOQC_CELL") { + script "modules/local/spoQC/cell/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "cellqc" + """ + } + } + + run("SPOQC_HQCR_IDENT") { + script "modules/local/spoQC/hqcr_ident/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqcr_ident" + input[2] = SPOQC_GENERAL.out.tmp.map { it[1] } + input[3] = SPOQC_BUBBLE.out.tmp.map { it[1] } + input[4] = SPOQC_DOUBLET.out.tmp.map { it[1] } + input[5] = SPOQC_VOID.out.tmp.map { it[1] } + input[6] = SPOQC_CELL.out.tmp.map { it[1] } + """ + } + } + + run("SPOQC_AMBIENT") { + script "modules/local/spoQC/ambient/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "ambientqc" + """ + } + } + + run("SPOQC_HQTR_METRICES") { + script "modules/local/spoQC/hqtr_metrices/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_metrices" + """ + } + } + + run("SPOQC_HQTR_QV") { + script "modules/local/spoQC/hqtr_qv/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_qv" + """ + } + } + + run("SPOQC_HQTR_AC") { + script "modules/local/spoQC/hqtr_ac/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_ac" + input[2] = SPOQC_AMBIENT.out.tmp.map { it[1] } + """ + } + } + + run("SPOQC_HQTR_CLUSTERING") { + script "modules/local/spoQC/hqtr_clustering/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_clustering" + input[2] = SPOQC_HQTR_METRICES.out.metrices.map { it[1] } + input[3] = SPOQC_HQTR_QV.out.tmp.map { it[1] } + input[4] = SPOQC_HQTR_AC.out.tmp.map { it[1] } + """ + } + } + + run("SPOQC_HQTR_REFINEMENT") { + script "modules/local/spoQC/hqtr_refinement/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_refinement" + input[2] = SPOQC_HQTR_CLUSTERING.out.mask.map { it[1] } + """ + } + } + + run("SPOQC_HQPR_METRICES") { + script "modules/local/spoQC/hqpr_metrices/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata.combine(channel.of(0)) + input[1] = "hqpr_metrices" + """ + } + } + + run("SPOQC_HQPR_CLUSTERING") { + script "modules/local/spoQC/hqpr_clustering/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata.combine(channel.of(0)) + input[1] = "hqpr_clustering" + input[2] = SPOQC_HQPR_METRICES.out.metrices.map { _meta, staining, f -> [staining, f] } + """ + } + } + + run("SPOQC_HQPR_REFINEMENT") { + script "modules/local/spoQC/hqpr_refinement/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata.combine(channel.of(0)) + input[1] = "hqpr_refinement" + input[2] = SPOQC_HQPR_CLUSTERING.out.mask.map { _meta, staining, f -> [staining, f] } + """ + } + } + } + + test("spoqc analysis_overview") { + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "analysis_overview" + input[3] = SPOQC_GENERAL.out.tmp.map { it[1] } + input[4] = SPOQC_BUBBLE.out.tmp.map { it[1] } + input[5] = SPOQC_DOUBLET.out.tmp.map { it[1] } + input[6] = SPOQC_VOID.out.tmp.map { it[1] } + input[7] = SPOQC_CELL.out.tmp.map { it[1] } + input[8] = SPOQC_HQCR_IDENT.out.mask.map { it[1] } + input[9] = SPOQC_HQCR_IDENT.out.mask_smoothed.map { it[1] } + input[10] = SPOQC_HQTR_QV.out.tmp.map { it[1] } + input[11] = SPOQC_HQTR_AC.out.tmp.map { it[1] } + input[12] = SPOQC_HQTR_METRICES.out.metrices.map { it[1] } + input[13] = SPOQC_HQTR_REFINEMENT.out.mask_smoothed.map { it[1] } + input[14] = SPOQC_HQTR_CLUSTERING.out.mask.map { it[1] } + input[15] = SPOQC_HQPR_METRICES.out.metrices.map { _meta, _staining, p -> p }.collect() + input[16] = SPOQC_HQPR_REFINEMENT.out.mask_smoothed.map { _meta, _staining, p -> p }.collect() + input[17] = SPOQC_HQPR_CLUSTERING.out.mask.map { _meta, _staining, p -> p }.collect() + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert file(process.out.h5ad[0][1]).exists() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + + test("spoqc analysis_overview stub") { + + options "-stub" + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "analysis_overview" + input[3] = SPOQC_GENERAL.out.tmp.map { it[1] } + input[4] = SPOQC_BUBBLE.out.tmp.map { it[1] } + input[5] = SPOQC_DOUBLET.out.tmp.map { it[1] } + input[6] = SPOQC_VOID.out.tmp.map { it[1] } + input[7] = SPOQC_CELL.out.tmp.map { it[1] } + input[8] = SPOQC_HQCR_IDENT.out.mask.map { it[1] } + input[9] = SPOQC_HQCR_IDENT.out.mask_smoothed.map { it[1] } + input[10] = SPOQC_HQTR_QV.out.tmp.map { it[1] } + input[11] = SPOQC_HQTR_AC.out.tmp.map { it[1] } + input[12] = SPOQC_HQTR_METRICES.out.metrices.map { it[1] } + input[13] = SPOQC_HQTR_REFINEMENT.out.mask_smoothed.map { it[1] } + input[14] = SPOQC_HQTR_CLUSTERING.out.mask.map { it[1] } + input[15] = SPOQC_HQPR_METRICES.out.metrices.map { _meta, _staining, p -> p }.collect() + input[16] = SPOQC_HQPR_REFINEMENT.out.mask_smoothed.map { _meta, _staining, p -> p }.collect() + input[17] = SPOQC_HQPR_CLUSTERING.out.mask.map { _meta, _staining, p -> p }.collect() + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert file(process.out.h5ad[0][1]).exists() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + +} diff --git a/modules/local/spoQC/analysis_overview/tests/main.nf.test.snap b/modules/local/spoQC/analysis_overview/tests/main.nf.test.snap new file mode 100644 index 00000000..d8886fa5 --- /dev/null +++ b/modules/local/spoQC/analysis_overview/tests/main.nf.test.snap @@ -0,0 +1,34 @@ +{ + "spoqc analysis_overview stub": { + "content": [ + [ + [ + "SPOQC_ANALYSIS_OVERVIEW", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T13:39:55.592097712", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + }, + "spoqc analysis_overview": { + "content": [ + [ + [ + "SPOQC_ANALYSIS_OVERVIEW", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T13:34:02.022856389", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + } +} \ No newline at end of file diff --git a/modules/local/spoQC/analysis_overview/tests/nextflow.config b/modules/local/spoQC/analysis_overview/tests/nextflow.config new file mode 100644 index 00000000..a39d075a --- /dev/null +++ b/modules/local/spoQC/analysis_overview/tests/nextflow.config @@ -0,0 +1,9 @@ +process { + + resourceLimits = [ + cpus: 4, + memory: '14.GB', + time: '2.h', + ] + +} diff --git a/modules/local/spoQC/annotation/main.nf b/modules/local/spoQC/annotation/main.nf new file mode 100644 index 00000000..6856850e --- /dev/null +++ b/modules/local/spoQC/annotation/main.nf @@ -0,0 +1,54 @@ + +process SPOQC_ANNOTATION { + tag "$meta.id" + label 'process_xl_cpus' + label 'process_mid_mem' + label 'process_high_time' + label 'spoqc' + + + container "heylf/spoqc:0.0.1" + + input: + tuple val(meta), path(spatialdata, stageAs: "*") + path(annotation, stageAs: "*") + val(step) + + output: + tuple val(meta), path("./report/annotation") , emit: report + tuple val(meta), path("./report/annotation/unsupervised_cell_annotation.tsv") , emit: annotation + tuple val("${task.process}"), val('spoqc'), eval("spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown"), topic: versions, emit: versions_spoqc + + when: + !annotation || (annotation instanceof List && annotation.isEmpty()) + + script: + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error("SPOQC_ANNOTATION module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + def args = task.ext.args ?: '' + + """ + python3 -m spoqc \\ + -i ${spatialdata} \\ + -o ./ \\ + -t ./spoQC_tmp/ \\ + -n ${task.cpus} \\ + -s ${step} \\ + --dataset ${meta.id} \\ + ${args} + """ + + stub: + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error("SPOQC_ANNOTATION module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + """ + mkdir -p ./report/annotation + touch ./report/annotation/unsupervised_cell_annotation.tsv + """ +} diff --git a/modules/local/spoQC/annotation/meta.yml b/modules/local/spoQC/annotation/meta.yml new file mode 100644 index 00000000..c34574f3 --- /dev/null +++ b/modules/local/spoQC/annotation/meta.yml @@ -0,0 +1,86 @@ +name: spoqc_annotation +description: | + Generate unsupervised cell-type annotation for a SpatialData object. + + Only runs when no cell-type annotation file was supplied upstream (`when: !annotation || (annotation instanceof List && annotation.isEmpty())`); otherwise the pipeline uses the user-supplied annotation directly. +keywords: + - spatial transcriptomics + - quality control + - spoqc + - xenium + - cell type annotation +tools: + - "spoqc": + description: | + spoQC is a modular framework for multimodal quality control (QC) of imaging-based spatially resolved transcriptomics (SRT). + homepage: https://github.com/heylf/spoQC + documentation: https://github.com/heylf/spoQC + licence: ["MIT license"] +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - spatialdata: + type: directory + description: Path to the SpatialData zarr store to run QC on + ontologies: [] + - annotation: + type: file + description: | + Optional cell-type annotation file. Not passed to the script; only consulted in the + process `when:` guard, which runs this module exclusively when no annotation was + supplied upstream (i.e. this input is always empty/absent whenever the process runs). + pattern: "*.tsv" + ontologies: [] + - step: + type: string + description: Name of the spoQC step to run, passed to `spoqc -s` +output: + report: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "report": + type: directory + description: HTML/plot QC report directory produced for this step + ontologies: [] + annotation: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "unsupervised_cell_annotation.tsv": + type: file + description: Unsupervised cell-type annotation table generated when no annotation was supplied. + pattern: "unsupervised_cell_annotation.tsv" + ontologies: [] + versions_spoqc: + - - "${task.process}": + type: string + description: The process the versions were collected from + - spoqc: + type: string + description: The tool name + - "spoqc --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 + - spoqc: + type: string + description: The tool name + - "spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown": + type: eval + description: The expression to obtain the version of the tool +authors: + - "@heylf" +maintainers: + - "@heylf" diff --git a/modules/local/spoQC/annotation/tests/main.nf.test b/modules/local/spoQC/annotation/tests/main.nf.test new file mode 100644 index 00000000..81a355ee --- /dev/null +++ b/modules/local/spoQC/annotation/tests/main.nf.test @@ -0,0 +1,84 @@ +nextflow_process { + + name "Test Process SPOQC_ANNOTATION" + script "../main.nf" + process "SPOQC_ANNOTATION" + config "./nextflow.config" + + tag "modules" + tag "modules_local" + tag "spoqc" + tag "spoqc/annotation" + + setup { + run("UNTAR") { + script "modules/nf-core/untar/main.nf" + process { + """ + input[0] = [[], file('https://raw.githubusercontent.com/nf-core/test-datasets/spatialaxe/xenium_bundle.tar.gz', checkIfExists: true)] + """ + } + } + + run("SPATIALDATA_WRITE") { + script "modules/local/spatialdata/write/main.nf" + process { + """ + input[0] = channel.of([ + [id: "test_run_spoqc"], + ]).combine(UNTAR.out.untar.map { it[1] }) + input[1] = "spatialdata_raw" + input[2] = [] + input[3] = "all" + """ + } + } + } + + test("spoqc annotation") { + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = [] + input[2] = "annotation" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert file(process.out.annotation[0][1]).exists() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + + test("spoqc annotation stub") { + + options "-stub" + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = [] + input[2] = "annotation" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert file(process.out.annotation[0][1]).exists() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + +} diff --git a/modules/local/spoQC/annotation/tests/main.nf.test.snap b/modules/local/spoQC/annotation/tests/main.nf.test.snap new file mode 100644 index 00000000..c0ce85c5 --- /dev/null +++ b/modules/local/spoQC/annotation/tests/main.nf.test.snap @@ -0,0 +1,34 @@ +{ + "spoqc annotation": { + "content": [ + [ + [ + "SPOQC_ANNOTATION", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T16:18:39.786273889", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + }, + "spoqc annotation stub": { + "content": [ + [ + [ + "SPOQC_ANNOTATION", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T16:19:03.283729223", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + } +} \ No newline at end of file diff --git a/modules/local/spoQC/annotation/tests/nextflow.config b/modules/local/spoQC/annotation/tests/nextflow.config new file mode 100644 index 00000000..f8b3a30a --- /dev/null +++ b/modules/local/spoQC/annotation/tests/nextflow.config @@ -0,0 +1,9 @@ +process { + + resourceLimits = [ + cpus: 4, + memory: '8.GB', + time: '2.h', + ] + +} diff --git a/modules/local/spoQC/bubble/main.nf b/modules/local/spoQC/bubble/main.nf new file mode 100644 index 00000000..83837050 --- /dev/null +++ b/modules/local/spoQC/bubble/main.nf @@ -0,0 +1,54 @@ + +process SPOQC_BUBBLE { + tag "$meta.id" + label 'process_tiny_cpus' + label 'process_tiny_mem' + label 'process_tiny_time' + label 'spoqc' + + + container "heylf/spoqc:0.0.1" + + input: + tuple val(meta), path(spatialdata, stageAs: "*") + val(step) + + output: + tuple val(meta), path("./report/bubbleqc") , emit: report + tuple val(meta), path("./spoQC_tmp/bubbleqc_output_hqcr.parquet") , emit: tmp + tuple val("${task.process}"), val('spoqc'), eval("spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown"), topic: versions, emit: versions_spoqc + + 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("SPOQC_BUBBLE module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + def args = task.ext.args ?: '' + + """ + python3 -m spoqc \\ + -i ${spatialdata} \\ + -o ./ \\ + -t ./spoQC_tmp/ \\ + -n ${task.cpus} \\ + -s ${step} \\ + --dataset ${meta.id} \\ + ${args} + """ + + stub: + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error("SPOQC_BUBBLE module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + """ + mkdir -p ./report/bubbleqc + mkdir -p ./spoQC_tmp + touch ./spoQC_tmp/bubbleqc_output_hqcr.parquet + """ +} diff --git a/modules/local/spoQC/bubble/meta.yml b/modules/local/spoQC/bubble/meta.yml new file mode 100644 index 00000000..60017403 --- /dev/null +++ b/modules/local/spoQC/bubble/meta.yml @@ -0,0 +1,77 @@ +name: spoqc_bubble +description: | + Run bubble-artifact QC checks on a SpatialData object. +keywords: + - spatial transcriptomics + - quality control + - spoqc + - xenium + - bubble artifact +tools: + - "spoqc": + description: | + spoQC is a modular framework for multimodal quality control (QC) of imaging-based spatially resolved transcriptomics (SRT). + homepage: https://github.com/heylf/spoQC + documentation: https://github.com/heylf/spoQC + licence: ["MIT license"] +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - spatialdata: + type: directory + description: Path to the SpatialData zarr store to run QC on + ontologies: [] + - step: + type: string + description: Name of the spoQC step to run, passed to `spoqc -s` +output: + report: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "report": + type: directory + description: HTML/plot QC report directory produced for this step + ontologies: [] + tmp: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "bubbleqc_output_hqcr.parquet": + type: file + description: | + Per-cell bubble-artifact QC parquet output, consumed by the HQCR identification/cell-type and downstream analysis steps. + pattern: "bubbleqc_output_hqcr.parquet" + ontologies: [] + versions_spoqc: + - - "${task.process}": + type: string + description: The process the versions were collected from + - spoqc: + type: string + description: The tool name + - "spoqc --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 + - spoqc: + type: string + description: The tool name + - "spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown": + type: eval + description: The expression to obtain the version of the tool +authors: + - "@heylf" +maintainers: + - "@heylf" diff --git a/modules/local/spoQC/bubble/tests/main.nf.test b/modules/local/spoQC/bubble/tests/main.nf.test new file mode 100644 index 00000000..02412adf --- /dev/null +++ b/modules/local/spoQC/bubble/tests/main.nf.test @@ -0,0 +1,82 @@ +nextflow_process { + + name "Test Process SPOQC_BUBBLE" + script "../main.nf" + process "SPOQC_BUBBLE" + config "./nextflow.config" + + tag "modules" + tag "modules_local" + tag "spoqc" + tag "spoqc/bubble" + + setup { + run("UNTAR") { + script "modules/nf-core/untar/main.nf" + process { + """ + input[0] = [[], file('https://raw.githubusercontent.com/nf-core/test-datasets/spatialaxe/xenium_bundle.tar.gz', checkIfExists: true)] + """ + } + } + + run("SPATIALDATA_WRITE") { + script "modules/local/spatialdata/write/main.nf" + process { + """ + input[0] = channel.of([ + [id: "test_run_spoqc"], + ]).combine(UNTAR.out.untar.map { it[1] }) + input[1] = "spatialdata_raw" + input[2] = [] + input[3] = "all" + """ + } + } + } + + test("spoqc bubble") { + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "bubbleqc" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert file(process.out.tmp[0][1]).exists() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + + test("spoqc bubble stub") { + + options "-stub" + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "bubbleqc" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert file(process.out.tmp[0][1]).exists() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + +} diff --git a/modules/local/spoQC/bubble/tests/main.nf.test.snap b/modules/local/spoQC/bubble/tests/main.nf.test.snap new file mode 100644 index 00000000..51d07ce7 --- /dev/null +++ b/modules/local/spoQC/bubble/tests/main.nf.test.snap @@ -0,0 +1,34 @@ +{ + "spoqc bubble": { + "content": [ + [ + [ + "SPOQC_BUBBLE", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T16:24:03.176288069", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + }, + "spoqc bubble stub": { + "content": [ + [ + [ + "SPOQC_BUBBLE", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T16:24:27.473999654", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + } +} \ No newline at end of file diff --git a/modules/local/spoQC/bubble/tests/nextflow.config b/modules/local/spoQC/bubble/tests/nextflow.config new file mode 100644 index 00000000..f8b3a30a --- /dev/null +++ b/modules/local/spoQC/bubble/tests/nextflow.config @@ -0,0 +1,9 @@ +process { + + resourceLimits = [ + cpus: 4, + memory: '8.GB', + time: '2.h', + ] + +} diff --git a/modules/local/spoQC/cell/main.nf b/modules/local/spoQC/cell/main.nf new file mode 100644 index 00000000..1a08264c --- /dev/null +++ b/modules/local/spoQC/cell/main.nf @@ -0,0 +1,55 @@ + +process SPOQC_CELL { + tag "$meta.id" + label 'process_tiny_cpus' + label 'process_low_mem' + label 'process_tiny_time' + label 'spoqc' + + + container "heylf/spoqc:0.0.1" + + input: + tuple val(meta), path(spatialdata, stageAs: "*") + val(step) + + output: + tuple val(meta), path("./report/cellqc") , emit: report + tuple val(meta), path("./spoQC_tmp/cellqc_output_hqcr.parquet") , emit: tmp + tuple val("${task.process}"), val('spoqc'), eval("spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown"), topic: versions, emit: versions_spoqc + + 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("SPOQC_CELL module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + def args = task.ext.args ?: '' + + """ + python3 -m spoqc \\ + -i ${spatialdata} \\ + -o ./ \\ + -t ./spoQC_tmp/ \\ + -n ${task.cpus} \\ + -s ${step} \\ + --dataset ${meta.id} \\ + ${args} + """ + + stub: + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error("SPOQC_CELL module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + """ + mkdir -p ./report/cellqc + mkdir -p ./spoQC_tmp + touch ./spoQC_tmp/cellqc_output_hqcr.parquet + """ + +} diff --git a/modules/local/spoQC/cell/meta.yml b/modules/local/spoQC/cell/meta.yml new file mode 100644 index 00000000..120fd9b7 --- /dev/null +++ b/modules/local/spoQC/cell/meta.yml @@ -0,0 +1,77 @@ +name: spoqc_cell +description: | + Run cell-level QC checks on a SpatialData object. +keywords: + - spatial transcriptomics + - quality control + - spoqc + - xenium + - cell qc +tools: + - "spoqc": + description: | + spoQC is a modular framework for multimodal quality control (QC) of imaging-based spatially resolved transcriptomics (SRT). + homepage: https://github.com/heylf/spoQC + documentation: https://github.com/heylf/spoQC + licence: ["MIT license"] +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - spatialdata: + type: directory + description: Path to the SpatialData zarr store to run QC on + ontologies: [] + - step: + type: string + description: Name of the spoQC step to run, passed to `spoqc -s` +output: + report: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "report": + type: directory + description: HTML/plot QC report directory produced for this step + ontologies: [] + tmp: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "cellqc_output_hqcr.parquet": + type: file + description: | + Per-cell QC parquet output, consumed by the HQCR identification/cell-type and downstream analysis steps. + pattern: "cellqc_output_hqcr.parquet" + ontologies: [] + versions_spoqc: + - - "${task.process}": + type: string + description: The process the versions were collected from + - spoqc: + type: string + description: The tool name + - "spoqc --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 + - spoqc: + type: string + description: The tool name + - "spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown": + type: eval + description: The expression to obtain the version of the tool +authors: + - "@heylf" +maintainers: + - "@heylf" diff --git a/modules/local/spoQC/cell/tests/main.nf.test b/modules/local/spoQC/cell/tests/main.nf.test new file mode 100644 index 00000000..41cb4cdd --- /dev/null +++ b/modules/local/spoQC/cell/tests/main.nf.test @@ -0,0 +1,82 @@ +nextflow_process { + + name "Test Process SPOQC_CELL" + script "../main.nf" + process "SPOQC_CELL" + config "./nextflow.config" + + tag "modules" + tag "modules_local" + tag "spoqc" + tag "spoqc/cell" + + setup { + run("UNTAR") { + script "modules/nf-core/untar/main.nf" + process { + """ + input[0] = [[], file('https://raw.githubusercontent.com/nf-core/test-datasets/spatialaxe/xenium_bundle.tar.gz', checkIfExists: true)] + """ + } + } + + run("SPATIALDATA_WRITE") { + script "modules/local/spatialdata/write/main.nf" + process { + """ + input[0] = channel.of([ + [id: "test_run_spoqc"], + ]).combine(UNTAR.out.untar.map { it[1] }) + input[1] = "spatialdata_raw" + input[2] = [] + input[3] = "all" + """ + } + } + } + + test("spoqc cell") { + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "cellqc" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert file(process.out.tmp[0][1]).exists() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + + test("spoqc cell stub") { + + options "-stub" + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "cellqc" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert file(process.out.tmp[0][1]).exists() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + +} diff --git a/modules/local/spoQC/cell/tests/main.nf.test.snap b/modules/local/spoQC/cell/tests/main.nf.test.snap new file mode 100644 index 00000000..31e11f4a --- /dev/null +++ b/modules/local/spoQC/cell/tests/main.nf.test.snap @@ -0,0 +1,34 @@ +{ + "spoqc cell stub": { + "content": [ + [ + [ + "SPOQC_CELL", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T16:27:32.200371067", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + }, + "spoqc cell": { + "content": [ + [ + [ + "SPOQC_CELL", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T16:27:08.600373919", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + } +} \ No newline at end of file diff --git a/modules/local/spoQC/cell/tests/nextflow.config b/modules/local/spoQC/cell/tests/nextflow.config new file mode 100644 index 00000000..f8b3a30a --- /dev/null +++ b/modules/local/spoQC/cell/tests/nextflow.config @@ -0,0 +1,9 @@ +process { + + resourceLimits = [ + cpus: 4, + memory: '8.GB', + time: '2.h', + ] + +} diff --git a/modules/local/spoQC/cellcycle/main.nf b/modules/local/spoQC/cellcycle/main.nf new file mode 100644 index 00000000..b8df29b4 --- /dev/null +++ b/modules/local/spoQC/cellcycle/main.nf @@ -0,0 +1,52 @@ + +process SPOQC_CELLCYCLE { + tag "$meta.id" + label 'process_tiny_cpus' + label 'process_low_mem' + label 'process_tiny_time' + label 'spoqc' + + + container "heylf/spoqc:0.0.1" + + input: + tuple val(meta), path(spatialdata, stageAs: "*") + val(step) + + output: + tuple val(meta), path("./report/cellcycleqc") , emit: report + tuple val("${task.process}"), val('spoqc'), eval("spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown"), topic: versions, emit: versions_spoqc + + 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("SPOQC_CELLCYCLE module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + def args = task.ext.args ?: '' + + """ + python3 -m spoqc \\ + -i ${spatialdata} \\ + -o ./ \\ + -t ./spoQC_tmp/ \\ + -n ${task.cpus} \\ + -s ${step} \\ + --dataset ${meta.id} \\ + ${args} + """ + + stub: + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error("SPOQC_CELLCYCLE module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + """ + mkdir -p ./report/cellcycleqc + """ + +} diff --git a/modules/local/spoQC/cellcycle/meta.yml b/modules/local/spoQC/cellcycle/meta.yml new file mode 100644 index 00000000..527a8f14 --- /dev/null +++ b/modules/local/spoQC/cellcycle/meta.yml @@ -0,0 +1,65 @@ +name: spoqc_cellcycle +description: | + Run cell-cycle scoring QC on a SpatialData object. +keywords: + - spatial transcriptomics + - quality control + - spoqc + - xenium + - cell cycle +tools: + - "spoqc": + description: | + spoQC is a modular framework for multimodal quality control (QC) of imaging-based spatially resolved transcriptomics (SRT). + homepage: https://github.com/heylf/spoQC + documentation: https://github.com/heylf/spoQC + licence: ["MIT license"] +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - spatialdata: + type: directory + description: Path to the SpatialData zarr store to run QC on + ontologies: [] + - step: + type: string + description: Name of the spoQC step to run, passed to `spoqc -s` +output: + report: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "report": + type: directory + description: HTML/plot QC report directory produced for this step + ontologies: [] + versions_spoqc: + - - "${task.process}": + type: string + description: The process the versions were collected from + - spoqc: + type: string + description: The tool name + - "spoqc --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 + - spoqc: + type: string + description: The tool name + - "spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown": + type: eval + description: The expression to obtain the version of the tool +authors: + - "@heylf" +maintainers: + - "@heylf" diff --git a/modules/local/spoQC/cellcycle/tests/main.nf.test b/modules/local/spoQC/cellcycle/tests/main.nf.test new file mode 100644 index 00000000..9689acba --- /dev/null +++ b/modules/local/spoQC/cellcycle/tests/main.nf.test @@ -0,0 +1,80 @@ +nextflow_process { + + name "Test Process SPOQC_CELLCYCLE" + script "../main.nf" + process "SPOQC_CELLCYCLE" + config "./nextflow.config" + + tag "modules" + tag "modules_local" + tag "spoqc" + tag "spoqc/cellcycle" + + setup { + run("UNTAR") { + script "modules/nf-core/untar/main.nf" + process { + """ + input[0] = [[], file('https://raw.githubusercontent.com/nf-core/test-datasets/spatialaxe/xenium_bundle.tar.gz', checkIfExists: true)] + """ + } + } + + run("SPATIALDATA_WRITE") { + script "modules/local/spatialdata/write/main.nf" + process { + """ + input[0] = channel.of([ + [id: "test_run_spoqc"], + ]).combine(UNTAR.out.untar.map { it[1] }) + input[1] = "spatialdata_raw" + input[2] = [] + input[3] = "all" + """ + } + } + } + + test("spoqc cellcycle") { + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "cellcycleqc" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + + test("spoqc cellcycle stub") { + + options "-stub" + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "cellcycleqc" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + +} diff --git a/modules/local/spoQC/cellcycle/tests/main.nf.test.snap b/modules/local/spoQC/cellcycle/tests/main.nf.test.snap new file mode 100644 index 00000000..e22f7bd6 --- /dev/null +++ b/modules/local/spoQC/cellcycle/tests/main.nf.test.snap @@ -0,0 +1,34 @@ +{ + "spoqc cellcycle stub": { + "content": [ + [ + [ + "SPOQC_CELLCYCLE", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T16:29:35.665483138", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + }, + "spoqc cellcycle": { + "content": [ + [ + [ + "SPOQC_CELLCYCLE", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T16:29:10.884938612", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + } +} \ No newline at end of file diff --git a/modules/local/spoQC/cellcycle/tests/nextflow.config b/modules/local/spoQC/cellcycle/tests/nextflow.config new file mode 100644 index 00000000..f8b3a30a --- /dev/null +++ b/modules/local/spoQC/cellcycle/tests/nextflow.config @@ -0,0 +1,9 @@ +process { + + resourceLimits = [ + cpus: 4, + memory: '8.GB', + time: '2.h', + ] + +} diff --git a/modules/local/spoQC/combine_masks/main.nf b/modules/local/spoQC/combine_masks/main.nf new file mode 100644 index 00000000..bab881a3 --- /dev/null +++ b/modules/local/spoQC/combine_masks/main.nf @@ -0,0 +1,58 @@ +process SPOQC_COMBINE_MASKS { + tag "${meta.id}_${staining}" + label 'process_mid_cpus' + label 'process_xl_mem' + label 'process_low_time' + label 'spoqc' + + + container "heylf/spoqc:0.0.1" + + input: + tuple val(meta), path(spatialdata, stageAs: "*"), val(_stain_sd) + val(step) + tuple path(mask_hqcr, stageAs: "spoQC_tmp/hqcr_output_mask_raw.parquet"), val(_stain_hqcr) + tuple val(staining), path(mask_hqpr, stageAs: "spoQC_tmp/*") + tuple path(mask_hqtr, stageAs: "spoQC_tmp/hqtr_output_mask_raw"), val(_stain_hqtr) + tuple path(mask_smoothed_hqcr, stageAs: "spoQC_tmp/hqcr_output_mask_smoothed_raw.parquet"), val(_stain_smoothed_hqcr) + tuple val(_s), path(mask_smoothed_hqpr, stageAs: "spoQC_tmp/*") + tuple path(mask_smoothed_hqtr, stageAs: "spoQC_tmp/hqtr_output_mask_smoothed_raw"), val(_stain_smoothed_hqtr) + + output: + tuple val(meta), path("report/combine_masks/${staining}") , emit: report + tuple val("${task.process}"), val('spoqc'), eval("spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown"), topic: versions, emit: versions_spoqc + + 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("SPOQC_COMBINE_MASKS module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + def args = task.ext.args ?: '' + + """ + python3 -m spoqc \\ + -i ${spatialdata} \\ + -o ./ \\ + -t ./spoQC_tmp/ \\ + -n ${task.cpus} \\ + -s ${step} \\ + --dataset ${meta.id} \\ + --staining ${staining} \\ + ${args} + """ + + stub: + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error("SPOQC_COMBINE_MASKS module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + """ + mkdir -p report/combine_masks/${staining} + """ + +} diff --git a/modules/local/spoQC/combine_masks/meta.yml b/modules/local/spoQC/combine_masks/meta.yml new file mode 100644 index 00000000..4c4eab4b --- /dev/null +++ b/modules/local/spoQC/combine_masks/meta.yml @@ -0,0 +1,117 @@ +name: spoqc_combine_masks +description: | + Combine the HQCR, HQPR and HQTR masks into a single per-staining QC report. +keywords: + - spatial transcriptomics + - quality control + - spoqc + - xenium + - mask combination +tools: + - "spoqc": + description: | + spoQC is a modular framework for multimodal quality control (QC) of imaging-based spatially resolved transcriptomics (SRT). + homepage: https://github.com/heylf/spoQC + documentation: https://github.com/heylf/spoQC + licence: ["MIT license"] +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - spatialdata: + type: directory + description: Path to the SpatialData zarr store to run QC on + ontologies: [] + - staining_idx_sd: + type: integer + description: Staining index carried alongside spatialdata to align the per-staining combined channel (Groovy var `_stain_sd`) + - step: + type: string + description: Name of the spoQC step to run, passed to `spoqc -s` + - - mask_hqcr: + type: file + description: HQCR raw mask from SPOQC_HQCR_IDENT + pattern: "hqcr_output_mask_raw.parquet" + ontologies: [] + - staining_idx_hqcr: + type: integer + description: Staining index carried alongside mask_hqcr to align the combined channel (Groovy var `_stain_hqcr`) + - - staining: + type: integer + description: Staining index + - mask_hqpr: + type: directory + description: Per-staining raw HQPR mask from SPOQC_HQPR_CLUSTERING + pattern: "spoQC_tmp/*" + ontologies: [] + - - mask_hqtr: + type: directory + description: HQTR raw mask from SPOQC_HQTR_CLUSTERING + pattern: "hqtr_output_mask_raw" + ontologies: [] + - staining_idx_hqtr: + type: integer + description: Staining index carried alongside mask_hqtr to align the combined channel (Groovy var `_stain_hqtr`) + - - mask_smoothed_hqcr: + type: file + description: Smoothed HQCR mask from SPOQC_HQCR_IDENT + pattern: "hqcr_output_mask_smoothed_raw.parquet" + ontologies: [] + - staining_idx_smoothed_hqcr: + type: integer + description: Staining index carried alongside mask_smoothed_hqcr to align the combined channel (Groovy var `_stain_smoothed_hqcr`) + - - staining_idx_smoothed_hqpr: + type: integer + description: Staining index carried alongside mask_smoothed_hqpr to align the combined channel (Groovy var `_s`) + - mask_smoothed_hqpr: + type: directory + description: Per-staining smoothed HQPR mask from SPOQC_HQPR_REFINEMENT + pattern: "spoQC_tmp/*" + ontologies: [] + - - mask_smoothed_hqtr: + type: directory + description: Smoothed HQTR mask from SPOQC_HQTR_REFINEMENT + pattern: "hqtr_output_mask_smoothed_raw" + ontologies: [] + - staining_idx_smoothed_hqtr: + type: integer + description: Staining index carried alongside mask_smoothed_hqtr to align the combined channel (Groovy var `_stain_smoothed_hqtr`) +output: + report: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "report/combine_masks/${staining}": + type: directory + description: HTML/plot QC report directory for this staining, tagged with the staining index + pattern: "report/combine_masks/${staining}" + ontologies: [] + versions_spoqc: + - - "${task.process}": + type: string + description: The process the versions were collected from + - spoqc: + type: string + description: The tool name + - "spoqc --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 + - spoqc: + type: string + description: The tool name + - "spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown": + type: eval + description: The expression to obtain the version of the tool +authors: + - "@heylf" +maintainers: + - "@heylf" diff --git a/modules/local/spoQC/combine_masks/tests/main.nf.test b/modules/local/spoQC/combine_masks/tests/main.nf.test new file mode 100644 index 00000000..bdbfb335 --- /dev/null +++ b/modules/local/spoQC/combine_masks/tests/main.nf.test @@ -0,0 +1,267 @@ +nextflow_process { + + name "Test Process SPOQC_COMBINE_MASKS" + script "../main.nf" + process "SPOQC_COMBINE_MASKS" + config "./nextflow.config" + + tag "modules" + tag "modules_local" + tag "spoqc" + tag "spoqc/combine_masks" + + setup { + run("UNTAR") { + script "modules/nf-core/untar/main.nf" + process { + """ + input[0] = [[], file('https://raw.githubusercontent.com/nf-core/test-datasets/spatialaxe/xenium_bundle.tar.gz', checkIfExists: true)] + """ + } + } + + run("SPATIALDATA_WRITE") { + script "modules/local/spatialdata/write/main.nf" + process { + """ + input[0] = channel.of([ + [id: "test_run_spoqc"], + ]).combine(UNTAR.out.untar.map { it[1] }) + input[1] = "spatialdata_raw" + input[2] = [] + input[3] = "all" + """ + } + } + + run("SPOQC_ANNOTATION") { + script "modules/local/spoQC/annotation/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = [] + input[2] = "annotation" + """ + } + } + + run("SPOQC_GENERAL") { + script "modules/local/spoQC/general/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "generalqc" + """ + } + } + + run("SPOQC_BUBBLE") { + script "modules/local/spoQC/bubble/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "bubbleqc" + """ + } + } + + run("SPOQC_DOUBLET") { + script "modules/local/spoQC/doublet/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "doubletqc" + """ + } + } + + run("SPOQC_VOID") { + script "modules/local/spoQC/void/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "voidqc" + """ + } + } + + run("SPOQC_CELL") { + script "modules/local/spoQC/cell/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "cellqc" + """ + } + } + + run("SPOQC_HQCR_IDENT") { + script "modules/local/spoQC/hqcr_ident/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqcr_ident" + input[2] = SPOQC_GENERAL.out.tmp.map { it[1] } + input[3] = SPOQC_BUBBLE.out.tmp.map { it[1] } + input[4] = SPOQC_DOUBLET.out.tmp.map { it[1] } + input[5] = SPOQC_VOID.out.tmp.map { it[1] } + input[6] = SPOQC_CELL.out.tmp.map { it[1] } + """ + } + } + + run("SPOQC_AMBIENT") { + script "modules/local/spoQC/ambient/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "ambientqc" + """ + } + } + + run("SPOQC_HQTR_METRICES") { + script "modules/local/spoQC/hqtr_metrices/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_metrices" + """ + } + } + + run("SPOQC_HQTR_QV") { + script "modules/local/spoQC/hqtr_qv/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_qv" + """ + } + } + + run("SPOQC_HQTR_AC") { + script "modules/local/spoQC/hqtr_ac/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_ac" + input[2] = SPOQC_AMBIENT.out.tmp.map { it[1] } + """ + } + } + + run("SPOQC_HQTR_CLUSTERING") { + script "modules/local/spoQC/hqtr_clustering/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_clustering" + input[2] = SPOQC_HQTR_METRICES.out.metrices.map { it[1] } + input[3] = SPOQC_HQTR_QV.out.tmp.map { it[1] } + input[4] = SPOQC_HQTR_AC.out.tmp.map { it[1] } + """ + } + } + + run("SPOQC_HQTR_REFINEMENT") { + script "modules/local/spoQC/hqtr_refinement/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_refinement" + input[2] = SPOQC_HQTR_CLUSTERING.out.mask.map { it[1] } + """ + } + } + + run("SPOQC_HQPR_METRICES") { + script "modules/local/spoQC/hqpr_metrices/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata.combine(channel.of(0)) + input[1] = "hqpr_metrices" + """ + } + } + + run("SPOQC_HQPR_CLUSTERING") { + script "modules/local/spoQC/hqpr_clustering/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata.combine(channel.of(0)) + input[1] = "hqpr_clustering" + input[2] = SPOQC_HQPR_METRICES.out.metrices.map { _meta, staining, f -> [staining, f] } + """ + } + } + + run("SPOQC_HQPR_REFINEMENT") { + script "modules/local/spoQC/hqpr_refinement/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata.combine(channel.of(0)) + input[1] = "hqpr_refinement" + input[2] = SPOQC_HQPR_CLUSTERING.out.mask.map { _meta, staining, f -> [staining, f] } + """ + } + } + } + + test("spoqc combine_masks") { + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata.combine(channel.of(0)) + input[1] = "combine_masks" + input[2] = SPOQC_HQCR_IDENT.out.mask.map { _meta, f -> [f, 0] } + input[3] = SPOQC_HQPR_CLUSTERING.out.mask.map { _meta, staining, f -> [staining, f] } + input[4] = SPOQC_HQTR_CLUSTERING.out.mask.map { _meta, f -> [f, 0] } + input[5] = SPOQC_HQCR_IDENT.out.mask_smoothed.map { _meta, f -> [f, 0] } + input[6] = SPOQC_HQPR_REFINEMENT.out.mask_smoothed.map { _meta, staining, f -> [staining, f] } + input[7] = SPOQC_HQTR_REFINEMENT.out.mask_smoothed.map { _meta, f -> [f, 0] } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + + test("spoqc combine_masks stub") { + + options "-stub" + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata.combine(channel.of(0)) + input[1] = "combine_masks" + input[2] = SPOQC_HQCR_IDENT.out.mask.map { _meta, f -> [f, 0] } + input[3] = SPOQC_HQPR_CLUSTERING.out.mask.map { _meta, staining, f -> [staining, f] } + input[4] = SPOQC_HQTR_CLUSTERING.out.mask.map { _meta, f -> [f, 0] } + input[5] = SPOQC_HQCR_IDENT.out.mask_smoothed.map { _meta, f -> [f, 0] } + input[6] = SPOQC_HQPR_REFINEMENT.out.mask_smoothed.map { _meta, staining, f -> [staining, f] } + input[7] = SPOQC_HQTR_REFINEMENT.out.mask_smoothed.map { _meta, f -> [f, 0] } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + +} diff --git a/modules/local/spoQC/combine_masks/tests/main.nf.test.snap b/modules/local/spoQC/combine_masks/tests/main.nf.test.snap new file mode 100644 index 00000000..49955e9f --- /dev/null +++ b/modules/local/spoQC/combine_masks/tests/main.nf.test.snap @@ -0,0 +1,34 @@ +{ + "spoqc combine_masks": { + "content": [ + [ + [ + "SPOQC_COMBINE_MASKS", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T16:08:49.679401464", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + }, + "spoqc combine_masks stub": { + "content": [ + [ + [ + "SPOQC_COMBINE_MASKS", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T16:12:54.234002727", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + } +} \ No newline at end of file diff --git a/modules/local/spoQC/combine_masks/tests/nextflow.config b/modules/local/spoQC/combine_masks/tests/nextflow.config new file mode 100644 index 00000000..f8b3a30a --- /dev/null +++ b/modules/local/spoQC/combine_masks/tests/nextflow.config @@ -0,0 +1,9 @@ +process { + + resourceLimits = [ + cpus: 4, + memory: '8.GB', + time: '2.h', + ] + +} diff --git a/modules/local/spoQC/doublet/main.nf b/modules/local/spoQC/doublet/main.nf new file mode 100644 index 00000000..552d45a2 --- /dev/null +++ b/modules/local/spoQC/doublet/main.nf @@ -0,0 +1,57 @@ + +process SPOQC_DOUBLET { + tag "$meta.id" + label 'process_high_cpus' + label 'process_low_mem' + label 'process_tiny_time' + label 'spoqc' + + + container "heylf/spoqc:0.0.1" + + input: + tuple val(meta), path(spatialdata, stageAs: "*") + path(annotation, stageAs: "*") + val(step) + + output: + tuple val(meta), path("./report/doubletqc") , emit: report + tuple val(meta), path("./spoQC_tmp/doubletqc_output_hqcr.parquet") , emit: tmp + tuple val("${task.process}"), val('spoqc'), eval("spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown"), topic: versions, emit: versions_spoqc + + 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("SPOQC_DOUBLET module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + def args = task.ext.args ?: '' + def annotation_arg = annotation ? "-a ${annotation}" : "" + + """ + python3 -m spoqc \\ + -i ${spatialdata} \\ + -o ./ \\ + -t ./spoQC_tmp/ \\ + -n ${task.cpus} \\ + ${annotation_arg} \\ + -s ${step} \\ + --dataset ${meta.id} \\ + ${args} + """ + + stub: + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error("SPOQC_DOUBLET module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + """ + mkdir -p ./report/doubletqc + mkdir -p ./spoQC_tmp + touch ./spoQC_tmp/doubletqc_output_hqcr.parquet + """ +} diff --git a/modules/local/spoQC/doublet/meta.yml b/modules/local/spoQC/doublet/meta.yml new file mode 100644 index 00000000..bb582bf2 --- /dev/null +++ b/modules/local/spoQC/doublet/meta.yml @@ -0,0 +1,83 @@ +name: spoqc_doublet +description: | + Run doublet-detection QC checks on a SpatialData object. +keywords: + - spatial transcriptomics + - quality control + - spoqc + - xenium + - doublet detection +tools: + - "spoqc": + description: | + spoQC is a modular framework for multimodal quality control (QC) of imaging-based spatially resolved transcriptomics (SRT). + homepage: https://github.com/heylf/spoQC + documentation: https://github.com/heylf/spoQC + licence: ["MIT license"] +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - spatialdata: + type: directory + description: Path to the SpatialData zarr store to run QC on + ontologies: [] + - annotation: + type: file + description: | + Optional cell-type annotation file. When empty/absent, spoqc is run without the `-a` flag. + pattern: "*.tsv" + ontologies: [] + - step: + type: string + description: Name of the spoQC step to run, passed to `spoqc -s` +output: + report: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "report": + type: directory + description: HTML/plot QC report directory produced for this step + ontologies: [] + tmp: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "doubletqc_output_hqcr.parquet": + type: file + description: | + Per-cell doublet QC parquet output, consumed by the HQCR identification/cell-type and downstream analysis steps. + pattern: "doubletqc_output_hqcr.parquet" + ontologies: [] + versions_spoqc: + - - "${task.process}": + type: string + description: The process the versions were collected from + - spoqc: + type: string + description: The tool name + - "spoqc --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 + - spoqc: + type: string + description: The tool name + - "spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown": + type: eval + description: The expression to obtain the version of the tool +authors: + - "@heylf" +maintainers: + - "@heylf" diff --git a/modules/local/spoQC/doublet/tests/main.nf.test b/modules/local/spoQC/doublet/tests/main.nf.test new file mode 100644 index 00000000..0b18a0c7 --- /dev/null +++ b/modules/local/spoQC/doublet/tests/main.nf.test @@ -0,0 +1,95 @@ +nextflow_process { + + name "Test Process SPOQC_DOUBLET" + script "../main.nf" + process "SPOQC_DOUBLET" + config "./nextflow.config" + + tag "modules" + tag "modules_local" + tag "spoqc" + tag "spoqc/doublet" + + setup { + run("UNTAR") { + script "modules/nf-core/untar/main.nf" + process { + """ + input[0] = [[], file('https://raw.githubusercontent.com/nf-core/test-datasets/spatialaxe/xenium_bundle.tar.gz', checkIfExists: true)] + """ + } + } + + run("SPATIALDATA_WRITE") { + script "modules/local/spatialdata/write/main.nf" + process { + """ + input[0] = channel.of([ + [id: "test_run_spoqc"], + ]).combine(UNTAR.out.untar.map { it[1] }) + input[1] = "spatialdata_raw" + input[2] = [] + input[3] = "all" + """ + } + } + + run("SPOQC_ANNOTATION") { + script "modules/local/spoQC/annotation/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = [] + input[2] = "annotation" + """ + } + } + } + + test("spoqc doublet") { + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "doubletqc" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert file(process.out.tmp[0][1]).exists() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + + test("spoqc doublet stub") { + + options "-stub" + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "doubletqc" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert file(process.out.tmp[0][1]).exists() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + +} diff --git a/modules/local/spoQC/doublet/tests/main.nf.test.snap b/modules/local/spoQC/doublet/tests/main.nf.test.snap new file mode 100644 index 00000000..c217861e --- /dev/null +++ b/modules/local/spoQC/doublet/tests/main.nf.test.snap @@ -0,0 +1,34 @@ +{ + "spoqc doublet": { + "content": [ + [ + [ + "SPOQC_DOUBLET", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T16:34:54.132198096", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + }, + "spoqc doublet stub": { + "content": [ + [ + [ + "SPOQC_DOUBLET", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T16:35:33.879268875", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + } +} \ No newline at end of file diff --git a/modules/local/spoQC/doublet/tests/nextflow.config b/modules/local/spoQC/doublet/tests/nextflow.config new file mode 100644 index 00000000..f8b3a30a --- /dev/null +++ b/modules/local/spoQC/doublet/tests/nextflow.config @@ -0,0 +1,9 @@ +process { + + resourceLimits = [ + cpus: 4, + memory: '8.GB', + time: '2.h', + ] + +} diff --git a/modules/local/spoQC/finalreport/main.nf b/modules/local/spoQC/finalreport/main.nf new file mode 100644 index 00000000..d9cac2fd --- /dev/null +++ b/modules/local/spoQC/finalreport/main.nf @@ -0,0 +1,76 @@ +process SPOQC_FINALREPORT { + tag "${meta.id}" + label 'process_tiny_cpus' + label 'process_low_mem' + label 'process_tiny_time' + label 'spoqc' + + + container "heylf/spoqc:0.0.1" + + input: + tuple val(meta), path(spatialdata, stageAs: "*") + val(step) + path(report_general, stageAs: "report/generalqc") + path(report_doublet, stageAs: "report/doubletqc") + path(report_void, stageAs: "report/voidqc") + path(report_cell, stageAs: "report/cellqc") + path(report_hqcr_ident, stageAs: "report/hqcr/hqcr_ident") + path(report_hqcr_celltype, stageAs: "report/hqcr/hqcr_celltype") + path(report_hqpr_metrices, stageAs: "report/hqpr/hqpr_metrices/*") + path(report_hqpr_clustering, stageAs: "report/hqpr/hqpr_clustering/*") + path(report_hqpr_refinement, stageAs: "report/hqpr/hqpr_refinement/*") + path(report_hqpr_bounding_box, stageAs: "report/hqpr/hqpr_bounding_box/*") + path(report_hqpr_celltype, stageAs: "report/hqpr/hqpr_celltype/*") + path(report_hqtr_metrices, stageAs: "report/hqtr/hqtr_metrices") + path(report_hqtr_ac, stageAs: "report/hqtr/hqtr_ac") + path(report_hqtr_qv, stageAs: "report/hqtr/hqtr_qv") + path(report_hqtr_clustering, stageAs: "report/hqtr/hqtr_clustering") + path(report_hqtr_refinement, stageAs: "report/hqtr/hqtr_refinement") + path(report_hqtr_bounding_box, stageAs: "report/hqtr/hqtr_bounding_box") + path(report_hqtr_celltype, stageAs: "report/hqtr/hqtr_celltype") + path(report_combine_masks, stageAs: "report/combine_masks/*") + path(report_transcript, stageAs: "report/transcriptqc") + path(report_cellcycle, stageAs: "report/cellcycleqc") + path(report_model, stageAs: "report/modelqc") + path(report_analysis_overview, stageAs: "report/analysis/overview") + path(report_analysis_category, stageAs: "report/analysis/category") + path(report_analysis_cluster, stageAs: "report/analysis/cluster") + + output: + tuple val(meta), path("report/report.html") , emit: report + tuple val("${task.process}"), val('spoqc'), eval("spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown"), topic: versions, emit: versions_spoqc + + 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("SPOQC_FINALREPORT module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + def args = task.ext.args ?: '' + + """ + python3 -m spoqc \\ + -i ${spatialdata} \\ + -o ./ \\ + -t ./spoQC_tmp/ \\ + -n ${task.cpus} \\ + -s ${step} \\ + --dataset ${meta.id} \\ + ${args} + """ + + stub: + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error("SPOQC_FINALREPORT module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + """ + mkdir -p report/ + touch report/report.html + """ +} diff --git a/modules/local/spoQC/finalreport/meta.yml b/modules/local/spoQC/finalreport/meta.yml new file mode 100644 index 00000000..1aa2ea3f --- /dev/null +++ b/modules/local/spoQC/finalreport/meta.yml @@ -0,0 +1,191 @@ +name: spoqc_finalreport +description: | + Generate the final combined spoQC HTML report, combining all upstream spoQC outputs. +keywords: + - spatial transcriptomics + - quality control + - spoqc + - xenium + - downstream analysis +tools: + - "spoqc": + description: | + spoQC is a modular framework for multimodal quality control (QC) of imaging-based spatially resolved transcriptomics (SRT). + homepage: https://github.com/heylf/spoQC + documentation: https://github.com/heylf/spoQC + licence: ["MIT license"] +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - spatialdata: + type: directory + description: Path to the SpatialData zarr store to run QC on + ontologies: [] + - step: + type: string + description: Name of the spoQC step to run, passed to `spoqc -s` + - report_general: + type: directory + description: Report fragment from SPOQC_GENERAL + pattern: "report/generalqc" + ontologies: [] + - report_doublet: + type: directory + description: Report fragment from SPOQC_DOUBLET + pattern: "report/doubletqc" + ontologies: [] + - report_void: + type: directory + description: Report fragment from SPOQC_VOID + pattern: "report/voidqc" + ontologies: [] + - report_cell: + type: directory + description: Report fragment from SPOQC_CELL + pattern: "report/cellqc" + ontologies: [] + - report_hqcr_ident: + type: directory + description: Report fragment from SPOQC_HQCR_IDENT + pattern: "report/hqcr/hqcr_ident" + ontologies: [] + - report_hqcr_celltype: + type: directory + description: Report fragment from SPOQC_HQCR_CELLTYPE + pattern: "report/hqcr/hqcr_celltype" + ontologies: [] + - report_hqpr_metrices: + type: directory + description: Collected per-staining report fragments from SPOQC_HQPR_METRICES + pattern: "report/hqpr/hqpr_metrices/*" + ontologies: [] + - report_hqpr_clustering: + type: directory + description: Collected per-staining report fragments from SPOQC_HQPR_CLUSTERING + pattern: "report/hqpr/hqpr_clustering/*" + ontologies: [] + - report_hqpr_refinement: + type: directory + description: Collected per-staining report fragments from SPOQC_HQPR_REFINEMENT + pattern: "report/hqpr/hqpr_refinement/*" + ontologies: [] + - report_hqpr_bounding_box: + type: directory + description: Collected per-staining report fragments from SPOQC_HQPR_BOUNDING_BOX + pattern: "report/hqpr/hqpr_bounding_box/*" + ontologies: [] + - report_hqpr_celltype: + type: directory + description: Report fragment from SPOQC_HQPR_CELLTYPE + pattern: "report/hqpr/hqpr_celltype/*" + ontologies: [] + - report_hqtr_metrices: + type: directory + description: Report fragment from SPOQC_HQTR_METRICES + pattern: "report/hqtr/hqtr_metrices" + ontologies: [] + - report_hqtr_ac: + type: directory + description: Report fragment from SPOQC_HQTR_AC + pattern: "report/hqtr/hqtr_ac" + ontologies: [] + - report_hqtr_qv: + type: directory + description: Report fragment from SPOQC_HQTR_QV + pattern: "report/hqtr/hqtr_qv" + ontologies: [] + - report_hqtr_clustering: + type: directory + description: Report fragment from SPOQC_HQTR_CLUSTERING + pattern: "report/hqtr/hqtr_clustering" + ontologies: [] + - report_hqtr_refinement: + type: directory + description: Report fragment from SPOQC_HQTR_REFINEMENT + pattern: "report/hqtr/hqtr_refinement" + ontologies: [] + - report_hqtr_bounding_box: + type: directory + description: Report fragment from SPOQC_HQTR_BOUNDING_BOX + pattern: "report/hqtr/hqtr_bounding_box" + ontologies: [] + - report_hqtr_celltype: + type: directory + description: Report fragment from SPOQC_HQTR_CELLTYPE + pattern: "report/hqtr/hqtr_celltype" + ontologies: [] + - report_combine_masks: + type: directory + description: Collected per-staining report fragments from SPOQC_COMBINE_MASKS + pattern: "report/combine_masks/*" + ontologies: [] + - report_transcript: + type: directory + description: Report fragment from SPOQC_TRANSCRIPT + pattern: "report/transcriptqc" + ontologies: [] + - report_cellcycle: + type: directory + description: Report fragment from SPOQC_CELLCYCLE + pattern: "report/cellcycleqc" + ontologies: [] + - report_model: + type: directory + description: Report fragment from SPOQC_MODEL + pattern: "report/modelqc" + ontologies: [] + - report_analysis_overview: + type: directory + description: Report fragment from SPOQC_ANALYSIS_OVERVIEW + pattern: "report/analysis/overview" + ontologies: [] + - report_analysis_category: + type: directory + description: Report fragment from SPOQC_ANALYSIS_CATEGORY + pattern: "report/analysis/category" + ontologies: [] + - report_analysis_cluster: + type: directory + description: Report fragment from SPOQC_ANALYSIS_CLUSTER + pattern: "report/analysis/cluster" + ontologies: [] +output: + report: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "report/report.html": + type: file + description: Final combined spoQC HTML report + pattern: "report/report.html" + ontologies: [] + versions_spoqc: + - - "${task.process}": + type: string + description: The process the versions were collected from + - spoqc: + type: string + description: The tool name + - "spoqc --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 + - spoqc: + type: string + description: The tool name + - "spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown": + type: eval + description: The expression to obtain the version of the tool +authors: + - "@heylf" +maintainers: + - "@heylf" diff --git a/modules/local/spoQC/finalreport/tests/main.nf.test b/modules/local/spoQC/finalreport/tests/main.nf.test new file mode 100644 index 00000000..75f20af4 --- /dev/null +++ b/modules/local/spoQC/finalreport/tests/main.nf.test @@ -0,0 +1,506 @@ +nextflow_process { + + name "Test Process SPOQC_FINALREPORT" + script "../main.nf" + process "SPOQC_FINALREPORT" + config "./nextflow.config" + + tag "modules" + tag "modules_local" + tag "spoqc" + tag "spoqc/finalreport" + + setup { + run("UNTAR") { + script "modules/nf-core/untar/main.nf" + process { + """ + input[0] = [[], file('https://raw.githubusercontent.com/nf-core/test-datasets/spatialaxe/xenium_bundle.tar.gz', checkIfExists: true)] + """ + } + } + + run("SPATIALDATA_WRITE") { + script "modules/local/spatialdata/write/main.nf" + process { + """ + input[0] = channel.of([ + [id: "test_run_spoqc"], + ]).combine(UNTAR.out.untar.map { it[1] }) + input[1] = "spatialdata_raw" + input[2] = [] + input[3] = "all" + """ + } + } + + run("SPOQC_ANNOTATION") { + script "modules/local/spoQC/annotation/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = [] + input[2] = "annotation" + """ + } + } + + run("SPOQC_GENERAL") { + script "modules/local/spoQC/general/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "generalqc" + """ + } + } + + run("SPOQC_BUBBLE") { + script "modules/local/spoQC/bubble/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "bubbleqc" + """ + } + } + + run("SPOQC_DOUBLET") { + script "modules/local/spoQC/doublet/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "doubletqc" + """ + } + } + + run("SPOQC_VOID") { + script "modules/local/spoQC/void/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "voidqc" + """ + } + } + + run("SPOQC_CELL") { + script "modules/local/spoQC/cell/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "cellqc" + """ + } + } + + run("SPOQC_HQCR_IDENT") { + script "modules/local/spoQC/hqcr_ident/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqcr_ident" + input[2] = SPOQC_GENERAL.out.tmp.map { it[1] } + input[3] = SPOQC_BUBBLE.out.tmp.map { it[1] } + input[4] = SPOQC_DOUBLET.out.tmp.map { it[1] } + input[5] = SPOQC_VOID.out.tmp.map { it[1] } + input[6] = SPOQC_CELL.out.tmp.map { it[1] } + """ + } + } + + run("SPOQC_HQCR_CELLTYPE") { + script "modules/local/spoQC/hqcr_celltype/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "hqcr_celltype" + input[3] = SPOQC_GENERAL.out.tmp.map { it[1] } + input[4] = SPOQC_BUBBLE.out.tmp.map { it[1] } + input[5] = SPOQC_DOUBLET.out.tmp.map { it[1] } + input[6] = SPOQC_VOID.out.tmp.map { it[1] } + input[7] = SPOQC_CELL.out.tmp.map { it[1] } + """ + } + } + + run("SPOQC_AMBIENT") { + script "modules/local/spoQC/ambient/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "ambientqc" + """ + } + } + + run("SPOQC_HQTR_METRICES") { + script "modules/local/spoQC/hqtr_metrices/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_metrices" + """ + } + } + + run("SPOQC_HQTR_QV") { + script "modules/local/spoQC/hqtr_qv/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_qv" + """ + } + } + + run("SPOQC_HQTR_AC") { + script "modules/local/spoQC/hqtr_ac/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_ac" + input[2] = SPOQC_AMBIENT.out.tmp.map { it[1] } + """ + } + } + + run("SPOQC_HQTR_CLUSTERING") { + script "modules/local/spoQC/hqtr_clustering/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_clustering" + input[2] = SPOQC_HQTR_METRICES.out.metrices.map { it[1] } + input[3] = SPOQC_HQTR_QV.out.tmp.map { it[1] } + input[4] = SPOQC_HQTR_AC.out.tmp.map { it[1] } + """ + } + } + + run("SPOQC_HQTR_REFINEMENT") { + script "modules/local/spoQC/hqtr_refinement/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_refinement" + input[2] = SPOQC_HQTR_CLUSTERING.out.mask.map { it[1] } + """ + } + } + + run("SPOQC_HQTR_BOUNDING_BOX") { + script "modules/local/spoQC/hqtr_bounding_box/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_bounding_box" + input[2] = SPOQC_HQTR_REFINEMENT.out.mask_smoothed.map { it[1] } + """ + } + } + + run("SPOQC_HQTR_CELLTYPE") { + script "modules/local/spoQC/hqtr_celltype/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "hqtr_celltype" + input[3] = SPOQC_GENERAL.out.tmp.map { it[1] } + input[4] = SPOQC_BUBBLE.out.tmp.map { it[1] } + input[5] = SPOQC_DOUBLET.out.tmp.map { it[1] } + input[6] = SPOQC_VOID.out.tmp.map { it[1] } + input[7] = SPOQC_CELL.out.tmp.map { it[1] } + input[8] = SPOQC_HQTR_CLUSTERING.out.mask.map { it[1] } + input[9] = SPOQC_HQTR_REFINEMENT.out.mask_smoothed.map { it[1] } + """ + } + } + + run("SPOQC_HQPR_METRICES") { + script "modules/local/spoQC/hqpr_metrices/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata.combine(channel.of(0)) + input[1] = "hqpr_metrices" + """ + } + } + + run("SPOQC_HQPR_CLUSTERING") { + script "modules/local/spoQC/hqpr_clustering/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata.combine(channel.of(0)) + input[1] = "hqpr_clustering" + input[2] = SPOQC_HQPR_METRICES.out.metrices.map { _meta, staining, f -> [staining, f] } + """ + } + } + + run("SPOQC_HQPR_REFINEMENT") { + script "modules/local/spoQC/hqpr_refinement/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata.combine(channel.of(0)) + input[1] = "hqpr_refinement" + input[2] = SPOQC_HQPR_CLUSTERING.out.mask.map { _meta, staining, f -> [staining, f] } + """ + } + } + + run("SPOQC_HQPR_BOUNDING_BOX") { + script "modules/local/spoQC/hqpr_bounding_box/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata.combine(channel.of(0)) + input[1] = "hqpr_bounding_box" + input[2] = SPOQC_HQPR_REFINEMENT.out.mask_smoothed.map { _meta, staining, f -> [staining, f] } + """ + } + } + + run("SPOQC_HQPR_CELLTYPE") { + script "modules/local/spoQC/hqpr_celltype/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata.combine(channel.of(0)) + input[1] = SPOQC_ANNOTATION.out.annotation.map { _meta, f -> [f, 0] } + input[2] = "hqpr_celltype" + input[3] = SPOQC_GENERAL.out.tmp.map { _meta, f -> [f, 0] } + input[4] = SPOQC_BUBBLE.out.tmp.map { _meta, f -> [f, 0] } + input[5] = SPOQC_DOUBLET.out.tmp.map { _meta, f -> [f, 0] } + input[6] = SPOQC_VOID.out.tmp.map { _meta, f -> [f, 0] } + input[7] = SPOQC_CELL.out.tmp.map { _meta, f -> [f, 0] } + input[8] = SPOQC_HQPR_CLUSTERING.out.mask.map { _meta, staining, m -> [staining, m] } + .join(SPOQC_HQPR_REFINEMENT.out.mask_smoothed.map { _meta, staining, m -> [staining, m] }) + """ + } + } + + run("SPOQC_COMBINE_MASKS") { + script "modules/local/spoQC/combine_masks/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata.combine(channel.of(0)) + input[1] = "combine_masks" + input[2] = SPOQC_HQCR_IDENT.out.mask.map { _meta, f -> [f, 0] } + input[3] = SPOQC_HQPR_CLUSTERING.out.mask.map { _meta, staining, f -> [staining, f] } + input[4] = SPOQC_HQTR_CLUSTERING.out.mask.map { _meta, f -> [f, 0] } + input[5] = SPOQC_HQCR_IDENT.out.mask_smoothed.map { _meta, f -> [f, 0] } + input[6] = SPOQC_HQPR_REFINEMENT.out.mask_smoothed.map { _meta, staining, f -> [staining, f] } + input[7] = SPOQC_HQTR_REFINEMENT.out.mask_smoothed.map { _meta, f -> [f, 0] } + """ + } + } + + run("SPOQC_TRANSCRIPT") { + script "modules/local/spoQC/transcript/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "transcriptqc" + """ + } + } + + run("SPOQC_CELLCYCLE") { + script "modules/local/spoQC/cellcycle/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "cellcycleqc" + """ + } + } + + run("SPOQC_MODEL") { + script "modules/local/spoQC/model/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "modelqc" + """ + } + } + + run("SPOQC_ANALYSIS_OVERVIEW") { + script "modules/local/spoQC/analysis_overview/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "analysis_overview" + input[3] = SPOQC_GENERAL.out.tmp.map { it[1] } + input[4] = SPOQC_BUBBLE.out.tmp.map { it[1] } + input[5] = SPOQC_DOUBLET.out.tmp.map { it[1] } + input[6] = SPOQC_VOID.out.tmp.map { it[1] } + input[7] = SPOQC_CELL.out.tmp.map { it[1] } + input[8] = SPOQC_HQCR_IDENT.out.mask.map { it[1] } + input[9] = SPOQC_HQCR_IDENT.out.mask_smoothed.map { it[1] } + input[10] = SPOQC_HQTR_QV.out.tmp.map { it[1] } + input[11] = SPOQC_HQTR_AC.out.tmp.map { it[1] } + input[12] = SPOQC_HQTR_METRICES.out.metrices.map { it[1] } + input[13] = SPOQC_HQTR_REFINEMENT.out.mask_smoothed.map { it[1] } + input[14] = SPOQC_HQTR_CLUSTERING.out.mask.map { it[1] } + input[15] = SPOQC_HQPR_METRICES.out.metrices.map { _meta, _staining, p -> p }.collect() + input[16] = SPOQC_HQPR_REFINEMENT.out.mask_smoothed.map { _meta, _staining, p -> p }.collect() + input[17] = SPOQC_HQPR_CLUSTERING.out.mask.map { _meta, _staining, p -> p }.collect() + """ + } + } + + run("SPOQC_ANALYSIS_CATEGORY") { + script "modules/local/spoQC/analysis_category/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "analysis_category" + input[3] = SPOQC_GENERAL.out.tmp.map { it[1] } + input[4] = SPOQC_BUBBLE.out.tmp.map { it[1] } + input[5] = SPOQC_DOUBLET.out.tmp.map { it[1] } + input[6] = SPOQC_VOID.out.tmp.map { it[1] } + input[7] = SPOQC_CELL.out.tmp.map { it[1] } + input[8] = SPOQC_HQCR_IDENT.out.mask.map { it[1] } + input[9] = SPOQC_HQCR_IDENT.out.mask_smoothed.map { it[1] } + input[10] = SPOQC_HQTR_QV.out.tmp.map { it[1] } + input[11] = SPOQC_HQTR_AC.out.tmp.map { it[1] } + input[12] = SPOQC_HQTR_METRICES.out.metrices.map { it[1] } + input[13] = SPOQC_HQTR_REFINEMENT.out.mask_smoothed.map { it[1] } + input[14] = SPOQC_HQTR_CLUSTERING.out.mask.map { it[1] } + input[15] = SPOQC_HQPR_METRICES.out.metrices.map { _meta, _staining, p -> p }.collect() + input[16] = SPOQC_HQPR_REFINEMENT.out.mask_smoothed.map { _meta, _staining, p -> p }.collect() + input[17] = SPOQC_HQPR_CLUSTERING.out.mask.map { _meta, _staining, p -> p }.collect() + """ + } + } + + run("SPOQC_ANALYSIS_CLUSTER") { + script "modules/local/spoQC/analysis_cluster/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "analysis_cluster" + input[3] = SPOQC_GENERAL.out.tmp.map { it[1] } + input[4] = SPOQC_BUBBLE.out.tmp.map { it[1] } + input[5] = SPOQC_DOUBLET.out.tmp.map { it[1] } + input[6] = SPOQC_VOID.out.tmp.map { it[1] } + input[7] = SPOQC_CELL.out.tmp.map { it[1] } + input[8] = SPOQC_HQCR_IDENT.out.mask.map { it[1] } + input[9] = SPOQC_HQCR_IDENT.out.mask_smoothed.map { it[1] } + input[10] = SPOQC_HQTR_QV.out.tmp.map { it[1] } + input[11] = SPOQC_HQTR_AC.out.tmp.map { it[1] } + input[12] = SPOQC_HQTR_METRICES.out.metrices.map { it[1] } + input[13] = SPOQC_HQTR_REFINEMENT.out.mask_smoothed.map { it[1] } + input[14] = SPOQC_HQTR_CLUSTERING.out.mask.map { it[1] } + input[15] = SPOQC_HQPR_METRICES.out.metrices.map { _meta, _staining, p -> p }.collect() + input[16] = SPOQC_HQPR_REFINEMENT.out.mask_smoothed.map { _meta, _staining, p -> p }.collect() + input[17] = SPOQC_HQPR_CLUSTERING.out.mask.map { _meta, _staining, p -> p }.collect() + """ + } + } + } + + test("spoqc finalreport") { + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "final_report" + input[2] = SPOQC_GENERAL.out.report.map { it[1] } + input[3] = SPOQC_DOUBLET.out.report.map { it[1] } + input[4] = SPOQC_VOID.out.report.map { it[1] } + input[5] = SPOQC_CELL.out.report.map { it[1] } + input[6] = SPOQC_HQCR_IDENT.out.report.map { it[1] } + input[7] = SPOQC_HQCR_CELLTYPE.out.report.map { it[1] } + input[8] = SPOQC_HQPR_METRICES.out.report.map { _meta, _staining, p -> p }.collect() + input[9] = SPOQC_HQPR_CLUSTERING.out.report.map { _meta, _staining, p -> p }.collect() + input[10] = SPOQC_HQPR_REFINEMENT.out.report.map { _meta, _staining, p -> p }.collect() + input[11] = SPOQC_HQPR_BOUNDING_BOX.out.report.map { _meta, _staining, p -> p }.collect() + input[12] = SPOQC_HQPR_CELLTYPE.out.report.map { it[1] }.collect() + input[13] = SPOQC_HQTR_METRICES.out.report.map { it[1] } + input[14] = SPOQC_HQTR_AC.out.report.map { it[1] } + input[15] = SPOQC_HQTR_QV.out.report.map { it[1] } + input[16] = SPOQC_HQTR_CLUSTERING.out.report.map { it[1] } + input[17] = SPOQC_HQTR_REFINEMENT.out.report.map { it[1] } + input[18] = SPOQC_HQTR_BOUNDING_BOX.out.report.map { it[1] } + input[19] = SPOQC_HQTR_CELLTYPE.out.report.map { it[1] } + input[20] = SPOQC_COMBINE_MASKS.out.report.map { it[1] }.collect() + input[21] = SPOQC_TRANSCRIPT.out.report.map { it[1] } + input[22] = SPOQC_CELLCYCLE.out.report.map { it[1] } + input[23] = SPOQC_MODEL.out.report.map { it[1] } + input[24] = SPOQC_ANALYSIS_OVERVIEW.out.report.map { it[1] } + input[25] = SPOQC_ANALYSIS_CATEGORY.out.report.map { it[1] } + input[26] = SPOQC_ANALYSIS_CLUSTER.out.report.map { it[1] } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).exists() }, + { assert file(process.out.report[0][1]).name == "report.html" }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + + test("spoqc finalreport stub") { + + options "-stub" + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "final_report" + input[2] = SPOQC_GENERAL.out.report.map { it[1] } + input[3] = SPOQC_DOUBLET.out.report.map { it[1] } + input[4] = SPOQC_VOID.out.report.map { it[1] } + input[5] = SPOQC_CELL.out.report.map { it[1] } + input[6] = SPOQC_HQCR_IDENT.out.report.map { it[1] } + input[7] = SPOQC_HQCR_CELLTYPE.out.report.map { it[1] } + input[8] = SPOQC_HQPR_METRICES.out.report.map { _meta, _staining, p -> p }.collect() + input[9] = SPOQC_HQPR_CLUSTERING.out.report.map { _meta, _staining, p -> p }.collect() + input[10] = SPOQC_HQPR_REFINEMENT.out.report.map { _meta, _staining, p -> p }.collect() + input[11] = SPOQC_HQPR_BOUNDING_BOX.out.report.map { _meta, _staining, p -> p }.collect() + input[12] = SPOQC_HQPR_CELLTYPE.out.report.map { it[1] }.collect() + input[13] = SPOQC_HQTR_METRICES.out.report.map { it[1] } + input[14] = SPOQC_HQTR_AC.out.report.map { it[1] } + input[15] = SPOQC_HQTR_QV.out.report.map { it[1] } + input[16] = SPOQC_HQTR_CLUSTERING.out.report.map { it[1] } + input[17] = SPOQC_HQTR_REFINEMENT.out.report.map { it[1] } + input[18] = SPOQC_HQTR_BOUNDING_BOX.out.report.map { it[1] } + input[19] = SPOQC_HQTR_CELLTYPE.out.report.map { it[1] } + input[20] = SPOQC_COMBINE_MASKS.out.report.map { it[1] }.collect() + input[21] = SPOQC_TRANSCRIPT.out.report.map { it[1] } + input[22] = SPOQC_CELLCYCLE.out.report.map { it[1] } + input[23] = SPOQC_MODEL.out.report.map { it[1] } + input[24] = SPOQC_ANALYSIS_OVERVIEW.out.report.map { it[1] } + input[25] = SPOQC_ANALYSIS_CATEGORY.out.report.map { it[1] } + input[26] = SPOQC_ANALYSIS_CLUSTER.out.report.map { it[1] } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).exists() }, + { assert file(process.out.report[0][1]).name == "report.html" }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + +} diff --git a/modules/local/spoQC/finalreport/tests/main.nf.test.snap b/modules/local/spoQC/finalreport/tests/main.nf.test.snap new file mode 100644 index 00000000..2fd17b0b --- /dev/null +++ b/modules/local/spoQC/finalreport/tests/main.nf.test.snap @@ -0,0 +1,34 @@ +{ + "spoqc finalreport": { + "content": [ + [ + [ + "SPOQC_FINALREPORT", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T15:33:10.087535508", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + }, + "spoqc finalreport stub": { + "content": [ + [ + [ + "SPOQC_FINALREPORT", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T15:42:10.451965739", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + } +} \ No newline at end of file diff --git a/modules/local/spoQC/finalreport/tests/nextflow.config b/modules/local/spoQC/finalreport/tests/nextflow.config new file mode 100644 index 00000000..a39d075a --- /dev/null +++ b/modules/local/spoQC/finalreport/tests/nextflow.config @@ -0,0 +1,9 @@ +process { + + resourceLimits = [ + cpus: 4, + memory: '14.GB', + time: '2.h', + ] + +} diff --git a/modules/local/spoQC/general/main.nf b/modules/local/spoQC/general/main.nf new file mode 100644 index 00000000..06f7c20c --- /dev/null +++ b/modules/local/spoQC/general/main.nf @@ -0,0 +1,58 @@ + +process SPOQC_GENERAL { + tag "$meta.id" + label 'process_tiny_cpus' + label 'process_low_mem' + label 'process_tiny_time' + label 'spoqc' + + + container "heylf/spoqc:0.0.1" + + input: + tuple val(meta), path(spatialdata, stageAs: "*") + path(annotation, stageAs: "*") + val(step) + + output: + tuple val(meta), path("./report/generalqc") , emit: report + tuple val(meta), path("./spoQC_tmp/generalqc_output_hqcr.parquet") , emit: tmp + tuple val("${task.process}"), val('spoqc'), eval("spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown"), topic: versions, emit: versions_spoqc + + 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("SPOQC_GENERAL module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + def args = task.ext.args ?: '' + def annotation_arg = annotation ? "-a ${annotation}" : "" + + """ + python3 -m spoqc \\ + -i ${spatialdata} \\ + -o ./ \\ + -t ./spoQC_tmp/ \\ + -n ${task.cpus} \\ + ${annotation_arg} \\ + -s ${step} \\ + --dataset ${meta.id} \\ + ${args} + """ + + stub: + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error("SPOQC_GENERAL module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + """ + mkdir -p ./report/generalqc + mkdir -p ./spoQC_tmp + touch ./spoQC_tmp/generalqc_output_hqcr.parquet + """ + +} diff --git a/modules/local/spoQC/general/meta.yml b/modules/local/spoQC/general/meta.yml new file mode 100644 index 00000000..728772d9 --- /dev/null +++ b/modules/local/spoQC/general/meta.yml @@ -0,0 +1,83 @@ +name: spoqc_general +description: | + Run general-purpose QC checks on a SpatialData object. +keywords: + - spatial transcriptomics + - quality control + - spoqc + - xenium + - general qc +tools: + - "spoqc": + description: | + spoQC is a modular framework for multimodal quality control (QC) of imaging-based spatially resolved transcriptomics (SRT). + homepage: https://github.com/heylf/spoQC + documentation: https://github.com/heylf/spoQC + licence: ["MIT license"] +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - spatialdata: + type: directory + description: Path to the SpatialData zarr store to run QC on + ontologies: [] + - annotation: + type: file + description: | + Optional cell-type annotation file. When empty/absent, spoqc is run without the `-a` flag. + pattern: "*.tsv" + ontologies: [] + - step: + type: string + description: Name of the spoQC step to run, passed to `spoqc -s` +output: + report: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "report": + type: directory + description: HTML/plot QC report directory produced for this step + ontologies: [] + tmp: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "generalqc_output_hqcr.parquet": + type: file + description: | + Per-cell general QC parquet output, consumed by the HQCR identification/cell-type and downstream analysis steps. + pattern: "generalqc_output_hqcr.parquet" + ontologies: [] + versions_spoqc: + - - "${task.process}": + type: string + description: The process the versions were collected from + - spoqc: + type: string + description: The tool name + - "spoqc --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 + - spoqc: + type: string + description: The tool name + - "spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown": + type: eval + description: The expression to obtain the version of the tool +authors: + - "@heylf" +maintainers: + - "@heylf" diff --git a/modules/local/spoQC/general/tests/main.nf.test b/modules/local/spoQC/general/tests/main.nf.test new file mode 100644 index 00000000..179d38a6 --- /dev/null +++ b/modules/local/spoQC/general/tests/main.nf.test @@ -0,0 +1,95 @@ +nextflow_process { + + name "Test Process SPOQC_GENERAL" + script "../main.nf" + process "SPOQC_GENERAL" + config "./nextflow.config" + + tag "modules" + tag "modules_local" + tag "spoqc" + tag "spoqc/general" + + setup { + run("UNTAR") { + script "modules/nf-core/untar/main.nf" + process { + """ + input[0] = [[], file('https://raw.githubusercontent.com/nf-core/test-datasets/spatialaxe/xenium_bundle.tar.gz', checkIfExists: true)] + """ + } + } + + run("SPATIALDATA_WRITE") { + script "modules/local/spatialdata/write/main.nf" + process { + """ + input[0] = channel.of([ + [id: "test_run_spoqc"], + ]).combine(UNTAR.out.untar.map { it[1] }) + input[1] = "spatialdata_raw" + input[2] = [] + input[3] = "all" + """ + } + } + + run("SPOQC_ANNOTATION") { + script "modules/local/spoQC/annotation/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = [] + input[2] = "annotation" + """ + } + } + } + + test("spoqc general") { + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "generalqc" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert file(process.out.tmp[0][1]).exists() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + + test("spoqc general stub") { + + options "-stub" + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "generalqc" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert file(process.out.tmp[0][1]).exists() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + +} diff --git a/modules/local/spoQC/general/tests/main.nf.test.snap b/modules/local/spoQC/general/tests/main.nf.test.snap new file mode 100644 index 00000000..42de643e --- /dev/null +++ b/modules/local/spoQC/general/tests/main.nf.test.snap @@ -0,0 +1,34 @@ +{ + "spoqc general": { + "content": [ + [ + [ + "SPOQC_GENERAL", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T16:44:00.440795369", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + }, + "spoqc general stub": { + "content": [ + [ + [ + "SPOQC_GENERAL", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T16:44:40.4934454", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + } +} \ No newline at end of file diff --git a/modules/local/spoQC/general/tests/nextflow.config b/modules/local/spoQC/general/tests/nextflow.config new file mode 100644 index 00000000..f8b3a30a --- /dev/null +++ b/modules/local/spoQC/general/tests/nextflow.config @@ -0,0 +1,9 @@ +process { + + resourceLimits = [ + cpus: 4, + memory: '8.GB', + time: '2.h', + ] + +} diff --git a/modules/local/spoQC/hqcr_celltype/main.nf b/modules/local/spoQC/hqcr_celltype/main.nf new file mode 100644 index 00000000..699f5cd2 --- /dev/null +++ b/modules/local/spoQC/hqcr_celltype/main.nf @@ -0,0 +1,63 @@ + +process SPOQC_HQCR_CELLTYPE { + tag "$meta.id" + label 'process_tiny_cpus' + label 'process_high_mem' + label 'process_tiny_time' + label 'spoqc' + + + container "heylf/spoqc:0.0.1" + + input: + tuple val(meta), path(spatialdata, stageAs: "*") + path(annotation, stageAs: "*") + val(step) + path(tmp_general, stageAs: "./spoQC_tmp/generalqc_output_hqcr.parquet") + path(tmp_bubble, stageAs: "./spoQC_tmp/bubbleqc_output_hqcr.parquet") + path(tmp_doublet, stageAs: "./spoQC_tmp/doubletqc_output_hqcr.parquet") + path(tmp_void, stageAs: "./spoQC_tmp/voidqc_output_hqcr.parquet") + path(tmp_cell, stageAs: "./spoQC_tmp/cellqc_output_hqcr.parquet") + + output: + tuple val(meta), path("./report/hqcr/hqcr_celltype") , emit: report + tuple val(meta), path("./spoQC_tmp/hqcr_output_mask_smoothed_celltype_refined.parquet") , emit: mask + tuple val("${task.process}"), val('spoqc'), eval("spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown"), topic: versions, emit: versions_spoqc + + 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("SPOQC_HQCR_CELLTYPE module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + def args = task.ext.args ?: '' + def annotation_arg = annotation ? "-a ${annotation}" : "" + + """ + python3 -m spoqc \\ + -i ${spatialdata} \\ + -o ./ \\ + -t ./spoQC_tmp/ \\ + -n ${task.cpus} \\ + ${annotation_arg} \\ + -s ${step} \\ + --dataset ${meta.id} \\ + ${args} + """ + + stub: + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error("SPOQC_HQCR_CELLTYPE module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + """ + mkdir -p ./report/hqcr/hqcr_celltype + mkdir -p ./spoQC_tmp + touch ./spoQC_tmp/hqcr_output_mask_smoothed_celltype_refined.parquet + """ + +} diff --git a/modules/local/spoQC/hqcr_celltype/meta.yml b/modules/local/spoQC/hqcr_celltype/meta.yml new file mode 100644 index 00000000..56ea2c68 --- /dev/null +++ b/modules/local/spoQC/hqcr_celltype/meta.yml @@ -0,0 +1,108 @@ +name: spoqc_hqcr_celltype +description: | + Refine the high-quality cell region (HQCR) mask using cell-type information, combining the general/bubble/doublet/void/cell QC outputs. +keywords: + - spatial transcriptomics + - quality control + - spoqc + - xenium + - hqcr + - cell type refinement +tools: + - "spoqc": + description: | + spoQC is a modular framework for multimodal quality control (QC) of imaging-based spatially resolved transcriptomics (SRT). + homepage: https://github.com/heylf/spoQC + documentation: https://github.com/heylf/spoQC + licence: ["MIT license"] +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - spatialdata: + type: directory + description: Path to the SpatialData zarr store to run QC on + ontologies: [] + - annotation: + type: file + description: | + Optional cell-type annotation file. When empty/absent, spoqc is run without the `-a` flag. + pattern: "*.tsv" + ontologies: [] + - step: + type: string + description: Name of the spoQC step to run, passed to `spoqc -s` + - tmp_general: + type: file + description: General QC parquet output from SPOQC_GENERAL + pattern: "generalqc_output_hqcr.parquet" + ontologies: [] + - tmp_bubble: + type: file + description: Bubble QC parquet output from SPOQC_BUBBLE + pattern: "bubbleqc_output_hqcr.parquet" + ontologies: [] + - tmp_doublet: + type: file + description: Doublet QC parquet output from SPOQC_DOUBLET + pattern: "doubletqc_output_hqcr.parquet" + ontologies: [] + - tmp_void: + type: file + description: Void QC parquet output from SPOQC_VOID + pattern: "voidqc_output_hqcr.parquet" + ontologies: [] + - tmp_cell: + type: file + description: Cell QC parquet output from SPOQC_CELL + pattern: "cellqc_output_hqcr.parquet" + ontologies: [] +output: + report: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "report": + type: directory + description: HTML/plot QC report directory produced for this step + ontologies: [] + mask: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "mask": + type: file + description: Cell-type-refined, smoothed HQCR mask + pattern: "hqcr_output_mask_smoothed_celltype_refined.parquet" + ontologies: [] + versions_spoqc: + - - "${task.process}": + type: string + description: The process the versions were collected from + - spoqc: + type: string + description: The tool name + - "spoqc --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 + - spoqc: + type: string + description: The tool name + - "spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown": + type: eval + description: The expression to obtain the version of the tool +authors: + - "@heylf" +maintainers: + - "@heylf" diff --git a/modules/local/spoQC/hqcr_celltype/tests/main.nf.test b/modules/local/spoQC/hqcr_celltype/tests/main.nf.test new file mode 100644 index 00000000..9c3b81f3 --- /dev/null +++ b/modules/local/spoQC/hqcr_celltype/tests/main.nf.test @@ -0,0 +1,157 @@ +nextflow_process { + + name "Test Process SPOQC_HQCR_CELLTYPE" + script "../main.nf" + process "SPOQC_HQCR_CELLTYPE" + config "./nextflow.config" + + tag "modules" + tag "modules_local" + tag "spoqc" + tag "spoqc/hqcr_celltype" + + setup { + run("UNTAR") { + script "modules/nf-core/untar/main.nf" + process { + """ + input[0] = [[], file('https://raw.githubusercontent.com/nf-core/test-datasets/spatialaxe/xenium_bundle.tar.gz', checkIfExists: true)] + """ + } + } + + run("SPATIALDATA_WRITE") { + script "modules/local/spatialdata/write/main.nf" + process { + """ + input[0] = channel.of([ + [id: "test_run_spoqc"], + ]).combine(UNTAR.out.untar.map { it[1] }) + input[1] = "spatialdata_raw" + input[2] = [] + input[3] = "all" + """ + } + } + + run("SPOQC_ANNOTATION") { + script "modules/local/spoQC/annotation/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = [] + input[2] = "annotation" + """ + } + } + + run("SPOQC_GENERAL") { + script "modules/local/spoQC/general/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "generalqc" + """ + } + } + + run("SPOQC_BUBBLE") { + script "modules/local/spoQC/bubble/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "bubbleqc" + """ + } + } + + run("SPOQC_DOUBLET") { + script "modules/local/spoQC/doublet/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "doubletqc" + """ + } + } + + run("SPOQC_VOID") { + script "modules/local/spoQC/void/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "voidqc" + """ + } + } + + run("SPOQC_CELL") { + script "modules/local/spoQC/cell/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "cellqc" + """ + } + } + } + + test("spoqc hqcr_celltype") { + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "hqcr_celltype" + input[3] = SPOQC_GENERAL.out.tmp.map { it[1] } + input[4] = SPOQC_BUBBLE.out.tmp.map { it[1] } + input[5] = SPOQC_DOUBLET.out.tmp.map { it[1] } + input[6] = SPOQC_VOID.out.tmp.map { it[1] } + input[7] = SPOQC_CELL.out.tmp.map { it[1] } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert file(process.out.mask[0][1]).exists() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + + test("spoqc hqcr_celltype stub") { + + options "-stub" + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "hqcr_celltype" + input[3] = SPOQC_GENERAL.out.tmp.map { it[1] } + input[4] = SPOQC_BUBBLE.out.tmp.map { it[1] } + input[5] = SPOQC_DOUBLET.out.tmp.map { it[1] } + input[6] = SPOQC_VOID.out.tmp.map { it[1] } + input[7] = SPOQC_CELL.out.tmp.map { it[1] } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert file(process.out.mask[0][1]).exists() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + +} diff --git a/modules/local/spoQC/hqcr_celltype/tests/main.nf.test.snap b/modules/local/spoQC/hqcr_celltype/tests/main.nf.test.snap new file mode 100644 index 00000000..9b1ae4a5 --- /dev/null +++ b/modules/local/spoQC/hqcr_celltype/tests/main.nf.test.snap @@ -0,0 +1,34 @@ +{ + "spoqc hqcr_celltype": { + "content": [ + [ + [ + "SPOQC_HQCR_CELLTYPE", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T16:50:20.155945817", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + }, + "spoqc hqcr_celltype stub": { + "content": [ + [ + [ + "SPOQC_HQCR_CELLTYPE", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T16:52:03.915308727", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + } +} \ No newline at end of file diff --git a/modules/local/spoQC/hqcr_celltype/tests/nextflow.config b/modules/local/spoQC/hqcr_celltype/tests/nextflow.config new file mode 100644 index 00000000..f8b3a30a --- /dev/null +++ b/modules/local/spoQC/hqcr_celltype/tests/nextflow.config @@ -0,0 +1,9 @@ +process { + + resourceLimits = [ + cpus: 4, + memory: '8.GB', + time: '2.h', + ] + +} diff --git a/modules/local/spoQC/hqcr_ident/main.nf b/modules/local/spoQC/hqcr_ident/main.nf new file mode 100644 index 00000000..f688b191 --- /dev/null +++ b/modules/local/spoQC/hqcr_ident/main.nf @@ -0,0 +1,61 @@ + +process SPOQC_HQCR_IDENT { + tag "$meta.id" + label 'process_tiny_cpus' + label 'process_high_mem' + label 'process_low_time' + label 'spoqc' + + + container "heylf/spoqc:0.0.1" + + input: + tuple val(meta), path(spatialdata, stageAs: "*") + val(step) + path(tmp_general, stageAs: "./spoQC_tmp/generalqc_output_hqcr.parquet") + path(tmp_bubble, stageAs: "./spoQC_tmp/bubbleqc_output_hqcr.parquet") + path(tmp_doublet, stageAs: "./spoQC_tmp/doubletqc_output_hqcr.parquet") + path(tmp_void, stageAs: "./spoQC_tmp/voidqc_output_hqcr.parquet") + path(tmp_cell, stageAs: "./spoQC_tmp/cellqc_output_hqcr.parquet") + + output: + tuple val(meta), path("./report/hqcr/hqcr_ident") , emit: report + tuple val(meta), path("./spoQC_tmp/hqcr_output_mask_raw.parquet") , emit: mask + tuple val(meta), path("./spoQC_tmp/hqcr_output_mask_smoothed_raw.parquet") , emit: mask_smoothed + tuple val("${task.process}"), val('spoqc'), eval("spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown"), topic: versions, emit: versions_spoqc + + 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("SPOQC_HQCR_IDENT module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + def args = task.ext.args ?: '' + + """ + python3 -m spoqc \\ + -i ${spatialdata} \\ + -o ./ \\ + -t ./spoQC_tmp/ \\ + -n ${task.cpus} \\ + -s ${step} \\ + --dataset ${meta.id} \\ + ${args} + """ + + stub: + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error("SPOQC_HQCR_IDENT module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + """ + mkdir -p ./report/hqcr/hqcr_ident + mkdir -p ./spoQC_tmp + touch ./spoQC_tmp/hqcr_output_mask_raw.parquet + touch ./spoQC_tmp/hqcr_output_mask_smoothed_raw.parquet + """ +} diff --git a/modules/local/spoQC/hqcr_ident/meta.yml b/modules/local/spoQC/hqcr_ident/meta.yml new file mode 100644 index 00000000..0ee3c596 --- /dev/null +++ b/modules/local/spoQC/hqcr_ident/meta.yml @@ -0,0 +1,113 @@ +name: spoqc_hqcr_ident +description: | + Identify high-quality cell regions (HQCR) by combining the general/bubble/doublet/void/cell QC outputs into a smoothed mask. +keywords: + - spatial transcriptomics + - quality control + - spoqc + - xenium + - hqcr + - high quality cell region +tools: + - "spoqc": + description: | + spoQC is a modular framework for multimodal quality control (QC) of imaging-based spatially resolved transcriptomics (SRT). + homepage: https://github.com/heylf/spoQC + documentation: https://github.com/heylf/spoQC + licence: ["MIT license"] +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - spatialdata: + type: directory + description: Path to the SpatialData zarr store to run QC on + ontologies: [] + - step: + type: string + description: Name of the spoQC step to run, passed to `spoqc -s` + - tmp_general: + type: file + description: General QC parquet output from SPOQC_GENERAL + pattern: "generalqc_output_hqcr.parquet" + ontologies: [] + - tmp_bubble: + type: file + description: Bubble QC parquet output from SPOQC_BUBBLE + pattern: "bubbleqc_output_hqcr.parquet" + ontologies: [] + - tmp_doublet: + type: file + description: Doublet QC parquet output from SPOQC_DOUBLET + pattern: "doubletqc_output_hqcr.parquet" + ontologies: [] + - tmp_void: + type: file + description: Void QC parquet output from SPOQC_VOID + pattern: "voidqc_output_hqcr.parquet" + ontologies: [] + - tmp_cell: + type: file + description: Cell QC parquet output from SPOQC_CELL + pattern: "cellqc_output_hqcr.parquet" + ontologies: [] +output: + report: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "report": + type: directory + description: HTML/plot QC report directory produced for this step + ontologies: [] + mask: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "mask": + type: file + description: Raw HQCR mask + pattern: "hqcr_output_mask_raw.parquet" + ontologies: [] + mask_smoothed: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "mask_smoothed": + type: file + description: Smoothed HQCR mask + pattern: "hqcr_output_mask_smoothed_raw.parquet" + ontologies: [] + versions_spoqc: + - - "${task.process}": + type: string + description: The process the versions were collected from + - spoqc: + type: string + description: The tool name + - "spoqc --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 + - spoqc: + type: string + description: The tool name + - "spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown": + type: eval + description: The expression to obtain the version of the tool +authors: + - "@heylf" +maintainers: + - "@heylf" diff --git a/modules/local/spoQC/hqcr_ident/tests/main.nf.test b/modules/local/spoQC/hqcr_ident/tests/main.nf.test new file mode 100644 index 00000000..80a5ff4a --- /dev/null +++ b/modules/local/spoQC/hqcr_ident/tests/main.nf.test @@ -0,0 +1,157 @@ +nextflow_process { + + name "Test Process SPOQC_HQCR_IDENT" + script "../main.nf" + process "SPOQC_HQCR_IDENT" + config "./nextflow.config" + + tag "modules" + tag "modules_local" + tag "spoqc" + tag "spoqc/hqcr_ident" + + setup { + run("UNTAR") { + script "modules/nf-core/untar/main.nf" + process { + """ + input[0] = [[], file('https://raw.githubusercontent.com/nf-core/test-datasets/spatialaxe/xenium_bundle.tar.gz', checkIfExists: true)] + """ + } + } + + run("SPATIALDATA_WRITE") { + script "modules/local/spatialdata/write/main.nf" + process { + """ + input[0] = channel.of([ + [id: "test_run_spoqc"], + ]).combine(UNTAR.out.untar.map { it[1] }) + input[1] = "spatialdata_raw" + input[2] = [] + input[3] = "all" + """ + } + } + + run("SPOQC_ANNOTATION") { + script "modules/local/spoQC/annotation/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = [] + input[2] = "annotation" + """ + } + } + + run("SPOQC_GENERAL") { + script "modules/local/spoQC/general/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "generalqc" + """ + } + } + + run("SPOQC_BUBBLE") { + script "modules/local/spoQC/bubble/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "bubbleqc" + """ + } + } + + run("SPOQC_DOUBLET") { + script "modules/local/spoQC/doublet/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "doubletqc" + """ + } + } + + run("SPOQC_VOID") { + script "modules/local/spoQC/void/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "voidqc" + """ + } + } + + run("SPOQC_CELL") { + script "modules/local/spoQC/cell/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "cellqc" + """ + } + } + } + + test("spoqc hqcr_ident") { + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqcr_ident" + input[2] = SPOQC_GENERAL.out.tmp.map { it[1] } + input[3] = SPOQC_BUBBLE.out.tmp.map { it[1] } + input[4] = SPOQC_DOUBLET.out.tmp.map { it[1] } + input[5] = SPOQC_VOID.out.tmp.map { it[1] } + input[6] = SPOQC_CELL.out.tmp.map { it[1] } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert file(process.out.mask[0][1]).exists() }, + { assert file(process.out.mask_smoothed[0][1]).exists() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + + test("spoqc hqcr_ident stub") { + + options "-stub" + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqcr_ident" + input[2] = SPOQC_GENERAL.out.tmp.map { it[1] } + input[3] = SPOQC_BUBBLE.out.tmp.map { it[1] } + input[4] = SPOQC_DOUBLET.out.tmp.map { it[1] } + input[5] = SPOQC_VOID.out.tmp.map { it[1] } + input[6] = SPOQC_CELL.out.tmp.map { it[1] } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert file(process.out.mask[0][1]).exists() }, + { assert file(process.out.mask_smoothed[0][1]).exists() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + +} diff --git a/modules/local/spoQC/hqcr_ident/tests/main.nf.test.snap b/modules/local/spoQC/hqcr_ident/tests/main.nf.test.snap new file mode 100644 index 00000000..6cd232e9 --- /dev/null +++ b/modules/local/spoQC/hqcr_ident/tests/main.nf.test.snap @@ -0,0 +1,34 @@ +{ + "spoqc hqcr_ident": { + "content": [ + [ + [ + "SPOQC_HQCR_IDENT", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T17:00:00.724431163", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + }, + "spoqc hqcr_ident stub": { + "content": [ + [ + [ + "SPOQC_HQCR_IDENT", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T17:01:43.922457923", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + } +} \ No newline at end of file diff --git a/modules/local/spoQC/hqcr_ident/tests/nextflow.config b/modules/local/spoQC/hqcr_ident/tests/nextflow.config new file mode 100644 index 00000000..f8b3a30a --- /dev/null +++ b/modules/local/spoQC/hqcr_ident/tests/nextflow.config @@ -0,0 +1,9 @@ +process { + + resourceLimits = [ + cpus: 4, + memory: '8.GB', + time: '2.h', + ] + +} diff --git a/modules/local/spoQC/hqpr_bounding_box/main.nf b/modules/local/spoQC/hqpr_bounding_box/main.nf new file mode 100644 index 00000000..a247284e --- /dev/null +++ b/modules/local/spoQC/hqpr_bounding_box/main.nf @@ -0,0 +1,54 @@ + +process SPOQC_HQPR_BOUNDING_BOX { + tag "${meta.id}_${staining}" + label 'process_high_cpus' + label 'process_high_mem' + label 'process_low_time' + label 'spoqc' + + + container "heylf/spoqc:0.0.1" + + input: + tuple val(meta), path(spatialdata, stageAs: "*"), val(_stain_sd) + val(step) + tuple val(staining), path(mask, stageAs: "spoQC_tmp/*") + + output: + tuple val(meta), val(staining), path("report/hqpr/hqpr_bounding_box/${staining}") , emit: report + tuple val("${task.process}"), val('spoqc'), eval("spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown"), topic: versions, emit: versions_spoqc + + 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("SPOQC_HQPR_BOUNDING_BOX module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + def args = task.ext.args ?: '' + + """ + python3 -m spoqc \\ + -i ${spatialdata} \\ + -o ./ \\ + -t ./spoQC_tmp/ \\ + -n ${task.cpus} \\ + -s ${step} \\ + --dataset ${meta.id} \\ + --staining ${staining} \\ + ${args} + """ + + stub: + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error("SPOQC_HQPR_BOUNDING_BOX module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + """ + mkdir -p report/hqpr/hqpr_bounding_box/${staining} + """ + +} diff --git a/modules/local/spoQC/hqpr_bounding_box/meta.yml b/modules/local/spoQC/hqpr_bounding_box/meta.yml new file mode 100644 index 00000000..3d28c286 --- /dev/null +++ b/modules/local/spoQC/hqpr_bounding_box/meta.yml @@ -0,0 +1,81 @@ +name: spoqc_hqpr_bounding_box +description: | + Generate a per-staining high-quality pixel region (HQPR) bounding-box QC report. +keywords: + - spatial transcriptomics + - quality control + - spoqc + - xenium + - hqpr + - bounding box +tools: + - "spoqc": + description: | + spoQC is a modular framework for multimodal quality control (QC) of imaging-based spatially resolved transcriptomics (SRT). + homepage: https://github.com/heylf/spoQC + documentation: https://github.com/heylf/spoQC + licence: ["MIT license"] +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - spatialdata: + type: directory + description: Path to the SpatialData zarr store to run QC on + ontologies: [] + - staining_idx_sd: + type: integer + description: Staining index carried alongside spatialdata to align the per-staining combined channel (Groovy var `_stain_sd`) + - step: + type: string + description: Name of the spoQC step to run, passed to `spoqc -s` + - - staining: + type: integer + description: Staining index + - mask: + type: directory + description: Per-staining smoothed HQPR mask from SPOQC_HQPR_REFINEMENT + pattern: "spoQC_tmp/*" + ontologies: [] +output: + report: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - staining: + type: integer + description: Staining index + - "report": + type: directory + description: HTML/plot QC report directory for this staining, tagged with the staining index + pattern: "report/hqpr/hqpr_bounding_box/${staining}" + ontologies: [] + versions_spoqc: + - - "${task.process}": + type: string + description: The process the versions were collected from + - spoqc: + type: string + description: The tool name + - "spoqc --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 + - spoqc: + type: string + description: The tool name + - "spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown": + type: eval + description: The expression to obtain the version of the tool +authors: + - "@heylf" +maintainers: + - "@heylf" diff --git a/modules/local/spoQC/hqpr_bounding_box/tests/main.nf.test b/modules/local/spoQC/hqpr_bounding_box/tests/main.nf.test new file mode 100644 index 00000000..1a0ea2c3 --- /dev/null +++ b/modules/local/spoQC/hqpr_bounding_box/tests/main.nf.test @@ -0,0 +1,114 @@ +nextflow_process { + + name "Test Process SPOQC_HQPR_BOUNDING_BOX" + script "../main.nf" + process "SPOQC_HQPR_BOUNDING_BOX" + config "./nextflow.config" + + tag "modules" + tag "modules_local" + tag "spoqc" + tag "spoqc/hqpr_bounding_box" + + setup { + run("UNTAR") { + script "modules/nf-core/untar/main.nf" + process { + """ + input[0] = [[], file('https://raw.githubusercontent.com/nf-core/test-datasets/spatialaxe/xenium_bundle.tar.gz', checkIfExists: true)] + """ + } + } + + run("SPATIALDATA_WRITE") { + script "modules/local/spatialdata/write/main.nf" + process { + """ + input[0] = channel.of([ + [id: "test_run_spoqc"], + ]).combine(UNTAR.out.untar.map { it[1] }) + input[1] = "spatialdata_raw" + input[2] = [] + input[3] = "all" + """ + } + } + + run("SPOQC_HQPR_METRICES") { + script "modules/local/spoQC/hqpr_metrices/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata.combine(channel.of(0)) + input[1] = "hqpr_metrices" + """ + } + } + + run("SPOQC_HQPR_CLUSTERING") { + script "modules/local/spoQC/hqpr_clustering/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata.combine(channel.of(0)) + input[1] = "hqpr_clustering" + input[2] = SPOQC_HQPR_METRICES.out.metrices.map { _meta, staining, f -> [staining, f] } + """ + } + } + + run("SPOQC_HQPR_REFINEMENT") { + script "modules/local/spoQC/hqpr_refinement/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata.combine(channel.of(0)) + input[1] = "hqpr_refinement" + input[2] = SPOQC_HQPR_CLUSTERING.out.mask.map { _meta, staining, f -> [staining, f] } + """ + } + } + } + + test("spoqc hqpr_bounding_box") { + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata.combine(channel.of(0)) + input[1] = "hqpr_bounding_box" + input[2] = SPOQC_HQPR_REFINEMENT.out.mask_smoothed.map { _meta, staining, f -> [staining, f] } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][2]).isDirectory() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + + test("spoqc hqpr_bounding_box stub") { + + options "-stub" + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata.combine(channel.of(0)) + input[1] = "hqpr_bounding_box" + input[2] = SPOQC_HQPR_REFINEMENT.out.mask_smoothed.map { _meta, staining, f -> [staining, f] } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][2]).isDirectory() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + +} diff --git a/modules/local/spoQC/hqpr_bounding_box/tests/main.nf.test.snap b/modules/local/spoQC/hqpr_bounding_box/tests/main.nf.test.snap new file mode 100644 index 00000000..4f4f8136 --- /dev/null +++ b/modules/local/spoQC/hqpr_bounding_box/tests/main.nf.test.snap @@ -0,0 +1,34 @@ +{ + "spoqc hqpr_bounding_box stub": { + "content": [ + [ + [ + "SPOQC_HQPR_BOUNDING_BOX", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T17:10:40.195231283", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + }, + "spoqc hqpr_bounding_box": { + "content": [ + [ + [ + "SPOQC_HQPR_BOUNDING_BOX", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T17:09:24.762245545", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + } +} \ No newline at end of file diff --git a/modules/local/spoQC/hqpr_bounding_box/tests/nextflow.config b/modules/local/spoQC/hqpr_bounding_box/tests/nextflow.config new file mode 100644 index 00000000..f8b3a30a --- /dev/null +++ b/modules/local/spoQC/hqpr_bounding_box/tests/nextflow.config @@ -0,0 +1,9 @@ +process { + + resourceLimits = [ + cpus: 4, + memory: '8.GB', + time: '2.h', + ] + +} diff --git a/modules/local/spoQC/hqpr_celltype/main.nf b/modules/local/spoQC/hqpr_celltype/main.nf new file mode 100644 index 00000000..856192b1 --- /dev/null +++ b/modules/local/spoQC/hqpr_celltype/main.nf @@ -0,0 +1,62 @@ + +process SPOQC_HQPR_CELLTYPE { + tag "${meta.id}_${staining}" + label 'process_tiny_cpus' + label 'process_high_mem' + label 'process_tiny_time' + label 'spoqc' + + + container "heylf/spoqc:0.0.1" + + input: + tuple val(meta), path(spatialdata, stageAs: "*"), val(_stain_sd) + tuple path(annotation, stageAs: "*"), val(_stain_ann) + val(step) + tuple path(tmp_general, stageAs: "./spoQC_tmp/generalqc_output_hqcr.parquet"), val(_stain_g) + tuple path(tmp_bubble, stageAs: "./spoQC_tmp/bubbleqc_output_hqcr.parquet"), val(_stain_b) + tuple path(tmp_doublet, stageAs: "./spoQC_tmp/doubletqc_output_hqcr.parquet"), val(_stain_d) + tuple path(tmp_void, stageAs: "./spoQC_tmp/voidqc_output_hqcr.parquet"), val(_stain_v) + tuple path(tmp_cell, stageAs: "./spoQC_tmp/cellqc_output_hqcr.parquet"), val(_stain_c) + tuple val(staining), path(mask, stageAs: "spoQC_tmp/*"), path(mask_smoothed, stageAs: "spoQC_tmp/*") + + output: + tuple val(meta), path("report/hqpr/hqpr_celltype/${staining}") , emit: report + tuple val("${task.process}"), val('spoqc'), eval("spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown"), topic: versions, emit: versions_spoqc + + 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("SPOQC_HQPR_CELLTYPE module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + def args = task.ext.args ?: '' + def annotation_arg = annotation ? "-a ${annotation}" : "" + + """ + python3 -m spoqc \\ + -i ${spatialdata} \\ + -o ./ \\ + -t ./spoQC_tmp/ \\ + -n ${task.cpus} \\ + ${annotation_arg} \\ + -s ${step} \\ + --dataset ${meta.id} \\ + --staining ${staining} \\ + ${args} + """ + + stub: + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error("SPOQC_HQPR_CELLTYPE module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + """ + mkdir -p report/hqpr/hqpr_celltype/${staining} + """ + +} diff --git a/modules/local/spoQC/hqpr_celltype/meta.yml b/modules/local/spoQC/hqpr_celltype/meta.yml new file mode 100644 index 00000000..0019fcfa --- /dev/null +++ b/modules/local/spoQC/hqpr_celltype/meta.yml @@ -0,0 +1,127 @@ +name: spoqc_hqpr_celltype +description: | + Run per-staining high-quality pixel region (HQPR) cell-type QC, combining the general/bubble/doublet/void/cell QC outputs with the HQPR probability and mask. +keywords: + - spatial transcriptomics + - quality control + - spoqc + - xenium + - hqpr + - cell type +tools: + - "spoqc": + description: | + spoQC is a modular framework for multimodal quality control (QC) of imaging-based spatially resolved transcriptomics (SRT). + homepage: https://github.com/heylf/spoQC + documentation: https://github.com/heylf/spoQC + licence: ["MIT license"] +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - spatialdata: + type: directory + description: Path to the SpatialData zarr store to run QC on + ontologies: [] + - staining_idx_sd: + type: integer + description: Staining index carried alongside spatialdata to align the per-staining combined channel (Groovy var `_stain_sd`) + - - annotation: + type: file + description: | + Optional cell-type annotation file. When empty/absent, spoqc is run without the `-a` flag. + pattern: "*.tsv" + ontologies: [] + - staining_idx_ann: + type: integer + description: Staining index carried alongside annotation to align the per-staining combined channel (Groovy var `_stain_ann`) + - step: + type: string + description: Name of the spoQC step to run, passed to `spoqc -s` + - - tmp_general: + type: file + description: General QC parquet output from SPOQC_GENERAL, tagged with the staining index + ontologies: [] + - staining_idx_g: + type: integer + description: Staining index carried alongside tmp_general to align the per-staining combined channel (Groovy var `_stain_g`) + - - tmp_bubble: + type: file + description: Bubble QC parquet output from SPOQC_BUBBLE, tagged with the staining index + ontologies: [] + - staining_idx_b: + type: integer + description: Staining index carried alongside tmp_bubble to align the per-staining combined channel (Groovy var `_stain_b`) + - - tmp_doublet: + type: file + description: Doublet QC parquet output from SPOQC_DOUBLET, tagged with the staining index + ontologies: [] + - staining_idx_d: + type: integer + description: Staining index carried alongside tmp_doublet to align the per-staining combined channel (Groovy var `_stain_d`) + - - tmp_void: + type: file + description: Void QC parquet output from SPOQC_VOID, tagged with the staining index + ontologies: [] + - staining_idx_v: + type: integer + description: Staining index carried alongside tmp_void to align the per-staining combined channel (Groovy var `_stain_v`) + - - tmp_cell: + type: file + description: Cell QC parquet output from SPOQC_CELL, tagged with the staining index + ontologies: [] + - staining_idx_c: + type: integer + description: Staining index carried alongside tmp_cell to align the per-staining combined channel (Groovy var `_stain_c`) + - - staining: + type: integer + description: Staining index + - mask: + type: directory + description: Per-staining HQPR raw probability mask from SPOQC_HQPR_CLUSTERING + pattern: "spoQC_tmp/*" + ontologies: [] + - mask_smoothed: + type: directory + description: Per-staining smoothed HQPR mask from SPOQC_HQPR_REFINEMENT + pattern: "spoQC_tmp/*" + ontologies: [] +output: + report: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "report": + type: directory + description: HTML/plot QC report directory for this staining, tagged with the staining index + pattern: "report/hqpr/hqpr_celltype/${staining}" + ontologies: [] + versions_spoqc: + - - "${task.process}": + type: string + description: The process the versions were collected from + - spoqc: + type: string + description: The tool name + - "spoqc --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 + - spoqc: + type: string + description: The tool name + - "spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown": + type: eval + description: The expression to obtain the version of the tool +authors: + - "@heylf" +maintainers: + - "@heylf" diff --git a/modules/local/spoQC/hqpr_celltype/tests/main.nf.test b/modules/local/spoQC/hqpr_celltype/tests/main.nf.test new file mode 100644 index 00000000..9751d92e --- /dev/null +++ b/modules/local/spoQC/hqpr_celltype/tests/main.nf.test @@ -0,0 +1,191 @@ +nextflow_process { + + name "Test Process SPOQC_HQPR_CELLTYPE" + script "../main.nf" + process "SPOQC_HQPR_CELLTYPE" + config "./nextflow.config" + + tag "modules" + tag "modules_local" + tag "spoqc" + tag "spoqc/hqpr_celltype" + + setup { + run("UNTAR") { + script "modules/nf-core/untar/main.nf" + process { + """ + input[0] = [[], file('https://raw.githubusercontent.com/nf-core/test-datasets/spatialaxe/xenium_bundle.tar.gz', checkIfExists: true)] + """ + } + } + + run("SPATIALDATA_WRITE") { + script "modules/local/spatialdata/write/main.nf" + process { + """ + input[0] = channel.of([ + [id: "test_run_spoqc"], + ]).combine(UNTAR.out.untar.map { it[1] }) + input[1] = "spatialdata_raw" + input[2] = [] + input[3] = "all" + """ + } + } + + run("SPOQC_ANNOTATION") { + script "modules/local/spoQC/annotation/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = [] + input[2] = "annotation" + """ + } + } + + run("SPOQC_GENERAL") { + script "modules/local/spoQC/general/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "generalqc" + """ + } + } + + run("SPOQC_BUBBLE") { + script "modules/local/spoQC/bubble/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "bubbleqc" + """ + } + } + + run("SPOQC_DOUBLET") { + script "modules/local/spoQC/doublet/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "doubletqc" + """ + } + } + + run("SPOQC_VOID") { + script "modules/local/spoQC/void/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "voidqc" + """ + } + } + + run("SPOQC_CELL") { + script "modules/local/spoQC/cell/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "cellqc" + """ + } + } + + run("SPOQC_HQPR_METRICES") { + script "modules/local/spoQC/hqpr_metrices/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata.combine(channel.of(0)) + input[1] = "hqpr_metrices" + """ + } + } + + run("SPOQC_HQPR_CLUSTERING") { + script "modules/local/spoQC/hqpr_clustering/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata.combine(channel.of(0)) + input[1] = "hqpr_clustering" + input[2] = SPOQC_HQPR_METRICES.out.metrices.map { _meta, staining, f -> [staining, f] } + """ + } + } + + run("SPOQC_HQPR_REFINEMENT") { + script "modules/local/spoQC/hqpr_refinement/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata.combine(channel.of(0)) + input[1] = "hqpr_refinement" + input[2] = SPOQC_HQPR_CLUSTERING.out.mask.map { _meta, staining, f -> [staining, f] } + """ + } + } + } + + test("spoqc hqpr_celltype") { + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata.combine(channel.of(0)) + input[1] = SPOQC_ANNOTATION.out.annotation.map { _meta, f -> [f, 0] } + input[2] = "hqpr_celltype" + input[3] = SPOQC_GENERAL.out.tmp.map { _meta, f -> [f, 0] } + input[4] = SPOQC_BUBBLE.out.tmp.map { _meta, f -> [f, 0] } + input[5] = SPOQC_DOUBLET.out.tmp.map { _meta, f -> [f, 0] } + input[6] = SPOQC_VOID.out.tmp.map { _meta, f -> [f, 0] } + input[7] = SPOQC_CELL.out.tmp.map { _meta, f -> [f, 0] } + input[8] = SPOQC_HQPR_CLUSTERING.out.mask.map { _meta, staining, m -> [staining, m] } + .join(SPOQC_HQPR_REFINEMENT.out.mask_smoothed.map { _meta, staining, m -> [staining, m] }) + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + + test("spoqc hqpr_celltype stub") { + + options "-stub" + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata.combine(channel.of(0)) + input[1] = SPOQC_ANNOTATION.out.annotation.map { _meta, f -> [f, 0] } + input[2] = "hqpr_celltype" + input[3] = SPOQC_GENERAL.out.tmp.map { _meta, f -> [f, 0] } + input[4] = SPOQC_BUBBLE.out.tmp.map { _meta, f -> [f, 0] } + input[5] = SPOQC_DOUBLET.out.tmp.map { _meta, f -> [f, 0] } + input[6] = SPOQC_VOID.out.tmp.map { _meta, f -> [f, 0] } + input[7] = SPOQC_CELL.out.tmp.map { _meta, f -> [f, 0] } + input[8] = SPOQC_HQPR_CLUSTERING.out.mask.map { _meta, staining, m -> [staining, m] } + .join(SPOQC_HQPR_REFINEMENT.out.mask_smoothed.map { _meta, staining, m -> [staining, m] }) + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + +} diff --git a/modules/local/spoQC/hqpr_celltype/tests/main.nf.test.snap b/modules/local/spoQC/hqpr_celltype/tests/main.nf.test.snap new file mode 100644 index 00000000..027fe72c --- /dev/null +++ b/modules/local/spoQC/hqpr_celltype/tests/main.nf.test.snap @@ -0,0 +1,34 @@ +{ + "spoqc hqpr_celltype": { + "content": [ + [ + [ + "SPOQC_HQPR_CELLTYPE", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T17:20:17.854497456", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + }, + "spoqc hqpr_celltype stub": { + "content": [ + [ + [ + "SPOQC_HQPR_CELLTYPE", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T17:22:47.557570783", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + } +} \ No newline at end of file diff --git a/modules/local/spoQC/hqpr_celltype/tests/nextflow.config b/modules/local/spoQC/hqpr_celltype/tests/nextflow.config new file mode 100644 index 00000000..f8b3a30a --- /dev/null +++ b/modules/local/spoQC/hqpr_celltype/tests/nextflow.config @@ -0,0 +1,9 @@ +process { + + resourceLimits = [ + cpus: 4, + memory: '8.GB', + time: '2.h', + ] + +} diff --git a/modules/local/spoQC/hqpr_clustering/main.nf b/modules/local/spoQC/hqpr_clustering/main.nf new file mode 100644 index 00000000..8c27b2cc --- /dev/null +++ b/modules/local/spoQC/hqpr_clustering/main.nf @@ -0,0 +1,56 @@ + +process SPOQC_HQPR_CLUSTERING { + tag "${meta.id}_${staining}" + label 'process_xl_cpus' + label 'process_xl_mem' + label 'process_xl_time' + label 'spoqc' + + + container "heylf/spoqc:0.0.1" + + input: + tuple val(meta), path(spatialdata, stageAs: "*"), val(_stain_sd) + val(step) + tuple val(staining), path(metrices, stageAs: "spoQC_tmp/metrices/hqpr/*") + + output: + tuple val(meta), val(staining), path("report/hqpr/hqpr_clustering/${staining}") , emit: report + tuple val(meta), val(staining), path("spoQC_tmp/hqpr_${staining}_output_mask_raw") , emit: mask + tuple val("${task.process}"), val('spoqc'), eval("spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown"), topic: versions, emit: versions_spoqc + + 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("SPOQC_HQPR_CLUSTERING module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + def args = task.ext.args ?: '' + + """ + python3 -m spoqc \\ + -i ${spatialdata} \\ + -o ./ \\ + -t ./spoQC_tmp/ \\ + -n ${task.cpus} \\ + -s ${step} \\ + --dataset ${meta.id} \\ + --staining ${staining} \\ + ${args} + """ + + stub: + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error("SPOQC_HQPR_CLUSTERING module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + """ + mkdir -p report/hqpr/hqpr_clustering/${staining} + mkdir -p spoQC_tmp/hqpr_${staining}_output_mask_raw + """ + +} diff --git a/modules/local/spoQC/hqpr_clustering/meta.yml b/modules/local/spoQC/hqpr_clustering/meta.yml new file mode 100644 index 00000000..545f52c3 --- /dev/null +++ b/modules/local/spoQC/hqpr_clustering/meta.yml @@ -0,0 +1,95 @@ +name: spoqc_hqpr_clustering +description: | + Cluster per-staining high-quality pixel region (HQPR) metrics into a probability mask. +keywords: + - spatial transcriptomics + - quality control + - spoqc + - xenium + - hqpr + - clustering +tools: + - "spoqc": + description: | + spoQC is a modular framework for multimodal quality control (QC) of imaging-based spatially resolved transcriptomics (SRT). + homepage: https://github.com/heylf/spoQC + documentation: https://github.com/heylf/spoQC + licence: ["MIT license"] +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - spatialdata: + type: directory + description: Path to the SpatialData zarr store to run QC on + ontologies: [] + - staining_idx_sd: + type: integer + description: Staining index carried alongside spatialdata to align the per-staining combined channel (Groovy var `_stain_sd`) + - step: + type: string + description: Name of the spoQC step to run, passed to `spoqc -s` + - - staining: + type: integer + description: Staining index + - metrices: + type: directory + description: Per-staining HQPR metrics directory from SPOQC_HQPR_METRICES + pattern: "spoQC_tmp/metrices/hqpr/*" + ontologies: [] +output: + report: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - staining: + type: integer + description: Staining index + - "report": + type: directory + description: HTML/plot QC report directory for this staining, tagged with the staining index + pattern: "report/hqpr/hqpr_clustering/${staining}" + ontologies: [] + mask: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - staining: + type: integer + description: Staining index + - "mask": + type: directory + description: Per-staining HQPR raw probability mask + pattern: "spoQC_tmp/hqpr_${staining}_output_mask_raw" + ontologies: [] + versions_spoqc: + - - "${task.process}": + type: string + description: The process the versions were collected from + - spoqc: + type: string + description: The tool name + - "spoqc --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 + - spoqc: + type: string + description: The tool name + - "spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown": + type: eval + description: The expression to obtain the version of the tool +authors: + - "@heylf" +maintainers: + - "@heylf" diff --git a/modules/local/spoQC/hqpr_clustering/tests/main.nf.test b/modules/local/spoQC/hqpr_clustering/tests/main.nf.test new file mode 100644 index 00000000..be2b98fe --- /dev/null +++ b/modules/local/spoQC/hqpr_clustering/tests/main.nf.test @@ -0,0 +1,94 @@ +nextflow_process { + + name "Test Process SPOQC_HQPR_CLUSTERING" + script "../main.nf" + process "SPOQC_HQPR_CLUSTERING" + config "./nextflow.config" + + tag "modules" + tag "modules_local" + tag "spoqc" + tag "spoqc/hqpr_clustering" + + setup { + run("UNTAR") { + script "modules/nf-core/untar/main.nf" + process { + """ + input[0] = [[], file('https://raw.githubusercontent.com/nf-core/test-datasets/spatialaxe/xenium_bundle.tar.gz', checkIfExists: true)] + """ + } + } + + run("SPATIALDATA_WRITE") { + script "modules/local/spatialdata/write/main.nf" + process { + """ + input[0] = channel.of([ + [id: "test_run_spoqc"], + ]).combine(UNTAR.out.untar.map { it[1] }) + input[1] = "spatialdata_raw" + input[2] = [] + input[3] = "all" + """ + } + } + + run("SPOQC_HQPR_METRICES") { + script "modules/local/spoQC/hqpr_metrices/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata.combine(channel.of(0)) + input[1] = "hqpr_metrices" + """ + } + } + } + + test("spoqc hqpr_clustering") { + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata.combine(channel.of(0)) + input[1] = "hqpr_clustering" + input[2] = SPOQC_HQPR_METRICES.out.metrices.map { _meta, staining, f -> [staining, f] } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][2]).isDirectory() }, + { assert file(process.out.mask[0][2]).isDirectory() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + + test("spoqc hqpr_clustering stub") { + + options "-stub" + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata.combine(channel.of(0)) + input[1] = "hqpr_clustering" + input[2] = SPOQC_HQPR_METRICES.out.metrices.map { _meta, staining, f -> [staining, f] } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][2]).isDirectory() }, + { assert file(process.out.mask[0][2]).isDirectory() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + +} diff --git a/modules/local/spoQC/hqpr_clustering/tests/main.nf.test.snap b/modules/local/spoQC/hqpr_clustering/tests/main.nf.test.snap new file mode 100644 index 00000000..1105a521 --- /dev/null +++ b/modules/local/spoQC/hqpr_clustering/tests/main.nf.test.snap @@ -0,0 +1,34 @@ +{ + "spoqc hqpr_clustering stub": { + "content": [ + [ + [ + "SPOQC_HQPR_CLUSTERING", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T17:28:23.695604129", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + }, + "spoqc hqpr_clustering": { + "content": [ + [ + [ + "SPOQC_HQPR_CLUSTERING", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T17:27:41.301167902", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + } +} \ No newline at end of file diff --git a/modules/local/spoQC/hqpr_clustering/tests/nextflow.config b/modules/local/spoQC/hqpr_clustering/tests/nextflow.config new file mode 100644 index 00000000..f8b3a30a --- /dev/null +++ b/modules/local/spoQC/hqpr_clustering/tests/nextflow.config @@ -0,0 +1,9 @@ +process { + + resourceLimits = [ + cpus: 4, + memory: '8.GB', + time: '2.h', + ] + +} diff --git a/modules/local/spoQC/hqpr_metrices/main.nf b/modules/local/spoQC/hqpr_metrices/main.nf new file mode 100644 index 00000000..8c6919e9 --- /dev/null +++ b/modules/local/spoQC/hqpr_metrices/main.nf @@ -0,0 +1,58 @@ + +process SPOQC_HQPR_METRICES { + tag "${meta.id}_${staining}" + label 'process_xl_cpus' + label 'process_xl_mem' + label 'process_mid_time' + label 'spoqc' + + + container "heylf/spoqc:0.0.1" + + input: + tuple val(meta), path(spatialdata, stageAs: "*"), val(staining) + val(step) + + output: + tuple val(meta), val(staining), path("report/hqpr/hqpr_metrices/${staining}") , emit: report + tuple val(meta), val(staining), path("spoQC_tmp/metrices/hqpr/${staining}") , emit: metrices + tuple val(meta), path("report/staining_log.txt") , emit: staininglog + tuple val("${task.process}"), val('spoqc'), eval("spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown"), topic: versions, emit: versions_spoqc + + 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("SPOQC_HQPR_METRICES module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + def args = task.ext.args ?: '' + + """ + python3 -m spoqc \\ + -i ${spatialdata} \\ + -o ./ \\ + -t ./spoQC_tmp/ \\ + -n ${task.cpus} \\ + -s ${step} \\ + --dataset ${meta.id} \\ + --staining ${staining} \\ + ${args} + """ + + stub: + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error("SPOQC_HQPR_METRICES module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + """ + mkdir -p report/hqpr/hqpr_metrices/${staining} + mkdir -p spoQC_tmp/metrices/hqpr/${staining} + mkdir -p report + touch report/staining_log.txt + """ + +} diff --git a/modules/local/spoQC/hqpr_metrices/meta.yml b/modules/local/spoQC/hqpr_metrices/meta.yml new file mode 100644 index 00000000..eb836b74 --- /dev/null +++ b/modules/local/spoQC/hqpr_metrices/meta.yml @@ -0,0 +1,98 @@ +name: spoqc_hqpr_metrices +description: | + Compute per-staining high-quality pixel region (HQPR) metrics for a SpatialData object. +keywords: + - spatial transcriptomics + - quality control + - spoqc + - xenium + - hqpr + - high quality pixel region +tools: + - "spoqc": + description: | + spoQC is a modular framework for multimodal quality control (QC) of imaging-based spatially resolved transcriptomics (SRT). + homepage: https://github.com/heylf/spoQC + documentation: https://github.com/heylf/spoQC + licence: ["MIT license"] +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - spatialdata: + type: directory + description: Path to the SpatialData zarr store to run QC on + ontologies: [] + - staining: + type: integer + description: Staining index to compute metrics for + - step: + type: string + description: Name of the spoQC step to run, passed to `spoqc -s` +output: + report: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - staining: + type: integer + description: Staining index + - "report": + type: directory + description: HTML/plot QC report directory for this staining, tagged with the staining index + pattern: "report/hqpr/hqpr_metrices/${staining}" + ontologies: [] + metrices: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - staining: + type: integer + description: Staining index + - "metrices": + type: directory + description: Per-staining HQPR metrics directory, tagged with the staining index + pattern: "spoQC_tmp/metrices/hqpr/${staining}" + ontologies: [] + staininglog: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "staininglog": + type: file + description: Log of stainings processed + pattern: "report/staining_log.txt" + ontologies: [] + versions_spoqc: + - - "${task.process}": + type: string + description: The process the versions were collected from + - spoqc: + type: string + description: The tool name + - "spoqc --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 + - spoqc: + type: string + description: The tool name + - "spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown": + type: eval + description: The expression to obtain the version of the tool +authors: + - "@heylf" +maintainers: + - "@heylf" diff --git a/modules/local/spoQC/hqpr_metrices/tests/main.nf.test b/modules/local/spoQC/hqpr_metrices/tests/main.nf.test new file mode 100644 index 00000000..d478dc8c --- /dev/null +++ b/modules/local/spoQC/hqpr_metrices/tests/main.nf.test @@ -0,0 +1,84 @@ +nextflow_process { + + name "Test Process SPOQC_HQPR_METRICES" + script "../main.nf" + process "SPOQC_HQPR_METRICES" + config "./nextflow.config" + + tag "modules" + tag "modules_local" + tag "spoqc" + tag "spoqc/hqpr_metrices" + + setup { + run("UNTAR") { + script "modules/nf-core/untar/main.nf" + process { + """ + input[0] = [[], file('https://raw.githubusercontent.com/nf-core/test-datasets/spatialaxe/xenium_bundle.tar.gz', checkIfExists: true)] + """ + } + } + + run("SPATIALDATA_WRITE") { + script "modules/local/spatialdata/write/main.nf" + process { + """ + input[0] = channel.of([ + [id: "test_run_spoqc"], + ]).combine(UNTAR.out.untar.map { it[1] }) + input[1] = "spatialdata_raw" + input[2] = [] + input[3] = "all" + """ + } + } + } + + test("spoqc hqpr_metrices") { + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata.combine(channel.of(0)) + input[1] = "hqpr_metrices" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][2]).isDirectory() }, + { assert file(process.out.metrices[0][2]).isDirectory() }, + { assert file(process.out.staininglog[0][1]).exists() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + + test("spoqc hqpr_metrices stub") { + + options "-stub" + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata.combine(channel.of(0)) + input[1] = "hqpr_metrices" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][2]).isDirectory() }, + { assert file(process.out.metrices[0][2]).isDirectory() }, + { assert file(process.out.staininglog[0][1]).exists() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + +} diff --git a/modules/local/spoQC/hqpr_metrices/tests/main.nf.test.snap b/modules/local/spoQC/hqpr_metrices/tests/main.nf.test.snap new file mode 100644 index 00000000..16cdff58 --- /dev/null +++ b/modules/local/spoQC/hqpr_metrices/tests/main.nf.test.snap @@ -0,0 +1,34 @@ +{ + "spoqc hqpr_metrices": { + "content": [ + [ + [ + "SPOQC_HQPR_METRICES", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T17:30:41.38387916", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + }, + "spoqc hqpr_metrices stub": { + "content": [ + [ + [ + "SPOQC_HQPR_METRICES", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T17:31:05.067509682", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + } +} \ No newline at end of file diff --git a/modules/local/spoQC/hqpr_metrices/tests/nextflow.config b/modules/local/spoQC/hqpr_metrices/tests/nextflow.config new file mode 100644 index 00000000..f8b3a30a --- /dev/null +++ b/modules/local/spoQC/hqpr_metrices/tests/nextflow.config @@ -0,0 +1,9 @@ +process { + + resourceLimits = [ + cpus: 4, + memory: '8.GB', + time: '2.h', + ] + +} diff --git a/modules/local/spoQC/hqpr_refinement/main.nf b/modules/local/spoQC/hqpr_refinement/main.nf new file mode 100644 index 00000000..f9f271d7 --- /dev/null +++ b/modules/local/spoQC/hqpr_refinement/main.nf @@ -0,0 +1,56 @@ + +process SPOQC_HQPR_REFINEMENT { + tag "${meta.id}_${staining}" + label 'process_xl_cpus' + label 'process_xl_mem' + label 'process_mid_time' + label 'spoqc' + + + container "heylf/spoqc:0.0.1" + + input: + tuple val(meta), path(spatialdata, stageAs: "*"), val(_stain_sd) + val(step) + tuple val(staining), path(mask, stageAs: "spoQC_tmp/*") + + output: + tuple val(meta), val(staining), path("report/hqpr/hqpr_refinement/${staining}") , emit: report + tuple val(meta), val(staining), path("spoQC_tmp/hqpr_${staining}_output_mask_smoothed_raw") , emit: mask_smoothed + tuple val("${task.process}"), val('spoqc'), eval("spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown"), topic: versions, emit: versions_spoqc + + 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("SPOQC_HQPR_REFINEMENT module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + def args = task.ext.args ?: '' + + """ + python3 -m spoqc \\ + -i ${spatialdata} \\ + -o ./ \\ + -t ./spoQC_tmp/ \\ + -n ${task.cpus} \\ + -s ${step} \\ + --dataset ${meta.id} \\ + --staining ${staining} \\ + ${args} + """ + + stub: + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error("SPOQC_HQPR_REFINEMENT module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + """ + mkdir -p report/hqpr/hqpr_refinement/${staining} + mkdir -p spoQC_tmp/hqpr_${staining}_output_mask_smoothed_raw + """ + +} diff --git a/modules/local/spoQC/hqpr_refinement/meta.yml b/modules/local/spoQC/hqpr_refinement/meta.yml new file mode 100644 index 00000000..1938dae4 --- /dev/null +++ b/modules/local/spoQC/hqpr_refinement/meta.yml @@ -0,0 +1,95 @@ +name: spoqc_hqpr_refinement +description: | + Refine the per-staining high-quality pixel region (HQPR) probability mask into a smoothed mask. +keywords: + - spatial transcriptomics + - quality control + - spoqc + - xenium + - hqpr + - mask refinement +tools: + - "spoqc": + description: | + spoQC is a modular framework for multimodal quality control (QC) of imaging-based spatially resolved transcriptomics (SRT). + homepage: https://github.com/heylf/spoQC + documentation: https://github.com/heylf/spoQC + licence: ["MIT license"] +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - spatialdata: + type: directory + description: Path to the SpatialData zarr store to run QC on + ontologies: [] + - staining_idx_sd: + type: integer + description: Staining index carried alongside spatialdata to align the per-staining combined channel (Groovy var `_stain_sd`) + - step: + type: string + description: Name of the spoQC step to run, passed to `spoqc -s` + - - staining: + type: integer + description: Staining index + - mask: + type: directory + description: Per-staining HQPR raw probability mask from SPOQC_HQPR_CLUSTERING + pattern: "spoQC_tmp/*" + ontologies: [] +output: + report: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - staining: + type: integer + description: Staining index + - "report": + type: directory + description: HTML/plot QC report directory for this staining, tagged with the staining index + pattern: "report/hqpr/hqpr_refinement/${staining}" + ontologies: [] + mask_smoothed: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - staining: + type: integer + description: Staining index + - "mask_smoothed": + type: directory + description: Per-staining smoothed HQPR mask + pattern: "spoQC_tmp/hqpr_${staining}_output_mask_smoothed_raw" + ontologies: [] + versions_spoqc: + - - "${task.process}": + type: string + description: The process the versions were collected from + - spoqc: + type: string + description: The tool name + - "spoqc --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 + - spoqc: + type: string + description: The tool name + - "spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown": + type: eval + description: The expression to obtain the version of the tool +authors: + - "@heylf" +maintainers: + - "@heylf" diff --git a/modules/local/spoQC/hqpr_refinement/tests/main.nf.test b/modules/local/spoQC/hqpr_refinement/tests/main.nf.test new file mode 100644 index 00000000..9ebfc8c1 --- /dev/null +++ b/modules/local/spoQC/hqpr_refinement/tests/main.nf.test @@ -0,0 +1,105 @@ +nextflow_process { + + name "Test Process SPOQC_HQPR_REFINEMENT" + script "../main.nf" + process "SPOQC_HQPR_REFINEMENT" + config "./nextflow.config" + + tag "modules" + tag "modules_local" + tag "spoqc" + tag "spoqc/hqpr_refinement" + + setup { + run("UNTAR") { + script "modules/nf-core/untar/main.nf" + process { + """ + input[0] = [[], file('https://raw.githubusercontent.com/nf-core/test-datasets/spatialaxe/xenium_bundle.tar.gz', checkIfExists: true)] + """ + } + } + + run("SPATIALDATA_WRITE") { + script "modules/local/spatialdata/write/main.nf" + process { + """ + input[0] = channel.of([ + [id: "test_run_spoqc"], + ]).combine(UNTAR.out.untar.map { it[1] }) + input[1] = "spatialdata_raw" + input[2] = [] + input[3] = "all" + """ + } + } + + run("SPOQC_HQPR_METRICES") { + script "modules/local/spoQC/hqpr_metrices/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata.combine(channel.of(0)) + input[1] = "hqpr_metrices" + """ + } + } + + run("SPOQC_HQPR_CLUSTERING") { + script "modules/local/spoQC/hqpr_clustering/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata.combine(channel.of(0)) + input[1] = "hqpr_clustering" + input[2] = SPOQC_HQPR_METRICES.out.metrices.map { _meta, staining, f -> [staining, f] } + """ + } + } + } + + test("spoqc hqpr_refinement") { + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata.combine(channel.of(0)) + input[1] = "hqpr_refinement" + input[2] = SPOQC_HQPR_CLUSTERING.out.mask.map { _meta, staining, f -> [staining, f] } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][2]).isDirectory() }, + { assert file(process.out.mask_smoothed[0][2]).isDirectory() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + + test("spoqc hqpr_refinement stub") { + + options "-stub" + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata.combine(channel.of(0)) + input[1] = "hqpr_refinement" + input[2] = SPOQC_HQPR_CLUSTERING.out.mask.map { _meta, staining, f -> [staining, f] } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][2]).isDirectory() }, + { assert file(process.out.mask_smoothed[0][2]).isDirectory() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + +} diff --git a/modules/local/spoQC/hqpr_refinement/tests/main.nf.test.snap b/modules/local/spoQC/hqpr_refinement/tests/main.nf.test.snap new file mode 100644 index 00000000..5b4db13a --- /dev/null +++ b/modules/local/spoQC/hqpr_refinement/tests/main.nf.test.snap @@ -0,0 +1,34 @@ +{ + "spoqc hqpr_refinement stub": { + "content": [ + [ + [ + "SPOQC_HQPR_REFINEMENT", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T17:39:43.296256843", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + }, + "spoqc hqpr_refinement": { + "content": [ + [ + [ + "SPOQC_HQPR_REFINEMENT", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T17:38:47.548789278", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + } +} \ No newline at end of file diff --git a/modules/local/spoQC/hqpr_refinement/tests/nextflow.config b/modules/local/spoQC/hqpr_refinement/tests/nextflow.config new file mode 100644 index 00000000..f8b3a30a --- /dev/null +++ b/modules/local/spoQC/hqpr_refinement/tests/nextflow.config @@ -0,0 +1,9 @@ +process { + + resourceLimits = [ + cpus: 4, + memory: '8.GB', + time: '2.h', + ] + +} diff --git a/modules/local/spoQC/hqtr_ac/main.nf b/modules/local/spoQC/hqtr_ac/main.nf new file mode 100644 index 00000000..81436a03 --- /dev/null +++ b/modules/local/spoQC/hqtr_ac/main.nf @@ -0,0 +1,55 @@ + +process SPOQC_HQTR_AC { + tag "$meta.id" + label 'process_xl_cpus' + label 'process_xl_mem' + label 'process_mid_time' + label 'spoqc' + + + container "heylf/spoqc:0.0.1" + + input: + tuple val(meta), path(spatialdata, stageAs: "*") + val(step) + path(ambient, stageAs: "./spoQC_tmp/ambient_output_genes.parquet") + + output: + tuple val(meta), path("./report/hqtr/hqtr_ac") , emit: report + tuple val(meta), path("./spoQC_tmp/hqtr_output_ac_prob") , emit: tmp + tuple val("${task.process}"), val('spoqc'), eval("spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown"), topic: versions, emit: versions_spoqc + + 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("SPOQC_HQTR_AC module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + def args = task.ext.args ?: '' + + """ + python3 -m spoqc \\ + -i ${spatialdata} \\ + -o ./ \\ + -t ./spoQC_tmp/ \\ + -n ${task.cpus} \\ + -s ${step} \\ + --dataset ${meta.id} \\ + ${args} + """ + + stub: + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error("SPOQC_HQTR_AC module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + """ + mkdir -p ./report/hqtr/hqtr_ac + mkdir -p ./spoQC_tmp/hqtr_output_ac_prob + """ + +} diff --git a/modules/local/spoQC/hqtr_ac/meta.yml b/modules/local/spoQC/hqtr_ac/meta.yml new file mode 100644 index 00000000..f6667dbe --- /dev/null +++ b/modules/local/spoQC/hqtr_ac/meta.yml @@ -0,0 +1,82 @@ +name: spoqc_hqtr_ac +description: | + Estimate ambient-contamination probability for high-quality transcript regions (HQTR), using the ambient-RNA QC output. +keywords: + - spatial transcriptomics + - quality control + - spoqc + - xenium + - hqtr + - ambient contamination +tools: + - "spoqc": + description: | + spoQC is a modular framework for multimodal quality control (QC) of imaging-based spatially resolved transcriptomics (SRT). + homepage: https://github.com/heylf/spoQC + documentation: https://github.com/heylf/spoQC + licence: ["MIT license"] +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - spatialdata: + type: directory + description: Path to the SpatialData zarr store to run QC on + ontologies: [] + - step: + type: string + description: Name of the spoQC step to run, passed to `spoqc -s` + - ambient: + type: file + description: Per-gene ambient-RNA QC parquet output from SPOQC_AMBIENT + pattern: "ambient_output_genes.parquet" + ontologies: [] +output: + report: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "report": + type: directory + description: HTML/plot QC report directory produced for this step + ontologies: [] + tmp: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "tmp": + type: directory + description: HQTR ambient-contamination probability output + pattern: "hqtr_output_ac_prob" + ontologies: [] + versions_spoqc: + - - "${task.process}": + type: string + description: The process the versions were collected from + - spoqc: + type: string + description: The tool name + - "spoqc --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 + - spoqc: + type: string + description: The tool name + - "spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown": + type: eval + description: The expression to obtain the version of the tool +authors: + - "@heylf" +maintainers: + - "@heylf" diff --git a/modules/local/spoQC/hqtr_ac/tests/main.nf.test b/modules/local/spoQC/hqtr_ac/tests/main.nf.test new file mode 100644 index 00000000..0ea4397d --- /dev/null +++ b/modules/local/spoQC/hqtr_ac/tests/main.nf.test @@ -0,0 +1,94 @@ +nextflow_process { + + name "Test Process SPOQC_HQTR_AC" + script "../main.nf" + process "SPOQC_HQTR_AC" + config "./nextflow.config" + + tag "modules" + tag "modules_local" + tag "spoqc" + tag "spoqc/hqtr_ac" + + setup { + run("UNTAR") { + script "modules/nf-core/untar/main.nf" + process { + """ + input[0] = [[], file('https://raw.githubusercontent.com/nf-core/test-datasets/spatialaxe/xenium_bundle.tar.gz', checkIfExists: true)] + """ + } + } + + run("SPATIALDATA_WRITE") { + script "modules/local/spatialdata/write/main.nf" + process { + """ + input[0] = channel.of([ + [id: "test_run_spoqc"], + ]).combine(UNTAR.out.untar.map { it[1] }) + input[1] = "spatialdata_raw" + input[2] = [] + input[3] = "all" + """ + } + } + + run("SPOQC_AMBIENT") { + script "modules/local/spoQC/ambient/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "ambientqc" + """ + } + } + } + + test("spoqc hqtr_ac") { + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_ac" + input[2] = SPOQC_AMBIENT.out.tmp.map { it[1] } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert file(process.out.tmp[0][1]).isDirectory() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + + test("spoqc hqtr_ac stub") { + + options "-stub" + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_ac" + input[2] = SPOQC_AMBIENT.out.tmp.map { it[1] } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert file(process.out.tmp[0][1]).isDirectory() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + +} diff --git a/modules/local/spoQC/hqtr_ac/tests/main.nf.test.snap b/modules/local/spoQC/hqtr_ac/tests/main.nf.test.snap new file mode 100644 index 00000000..e2e1cac2 --- /dev/null +++ b/modules/local/spoQC/hqtr_ac/tests/main.nf.test.snap @@ -0,0 +1,34 @@ +{ + "spoqc hqtr_ac": { + "content": [ + [ + [ + "SPOQC_HQTR_AC", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T17:43:53.230451909", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + }, + "spoqc hqtr_ac stub": { + "content": [ + [ + [ + "SPOQC_HQTR_AC", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T17:44:33.559586317", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + } +} \ No newline at end of file diff --git a/modules/local/spoQC/hqtr_ac/tests/nextflow.config b/modules/local/spoQC/hqtr_ac/tests/nextflow.config new file mode 100644 index 00000000..f8b3a30a --- /dev/null +++ b/modules/local/spoQC/hqtr_ac/tests/nextflow.config @@ -0,0 +1,9 @@ +process { + + resourceLimits = [ + cpus: 4, + memory: '8.GB', + time: '2.h', + ] + +} diff --git a/modules/local/spoQC/hqtr_bounding_box/main.nf b/modules/local/spoQC/hqtr_bounding_box/main.nf new file mode 100644 index 00000000..6036e63b --- /dev/null +++ b/modules/local/spoQC/hqtr_bounding_box/main.nf @@ -0,0 +1,52 @@ +process SPOQC_HQTR_BOUNDING_BOX { + tag "$meta.id" + label 'process_high_cpus' + label 'process_high_mem' + label 'process_low_time' + label 'spoqc' + + + container "heylf/spoqc:0.0.1" + + input: + tuple val(meta), path(spatialdata, stageAs: "*") + val(step) + path(edge_strength, stageAs: "./spoQC_tmp/hqtr_output_mask_smoothed_raw") + + output: + tuple val(meta), path("./report/hqtr/hqtr_bounding_box") , emit: report + tuple val("${task.process}"), val('spoqc'), eval("spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown"), topic: versions, emit: versions_spoqc + + 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("SPOQC_HQTR_BOUNDING_BOX module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + def args = task.ext.args ?: '' + + """ + python3 -m spoqc \\ + -i ${spatialdata} \\ + -o ./ \\ + -t ./spoQC_tmp/ \\ + -n ${task.cpus} \\ + -s ${step} \\ + --dataset ${meta.id} \\ + ${args} + """ + + stub: + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error("SPOQC_HQTR_BOUNDING_BOX module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + """ + mkdir -p ./report/hqtr/hqtr_bounding_box + """ + +} diff --git a/modules/local/spoQC/hqtr_bounding_box/meta.yml b/modules/local/spoQC/hqtr_bounding_box/meta.yml new file mode 100644 index 00000000..d37d72e0 --- /dev/null +++ b/modules/local/spoQC/hqtr_bounding_box/meta.yml @@ -0,0 +1,71 @@ +name: spoqc_hqtr_bounding_box +description: | + Generate a high-quality transcript region (HQTR) bounding-box QC report. +keywords: + - spatial transcriptomics + - quality control + - spoqc + - xenium + - hqtr + - bounding box +tools: + - "spoqc": + description: | + spoQC is a modular framework for multimodal quality control (QC) of imaging-based spatially resolved transcriptomics (SRT). + homepage: https://github.com/heylf/spoQC + documentation: https://github.com/heylf/spoQC + licence: ["MIT license"] +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - spatialdata: + type: directory + description: Path to the SpatialData zarr store to run QC on + ontologies: [] + - step: + type: string + description: Name of the spoQC step to run, passed to `spoqc -s` + - edge_strength: + type: directory + description: HQTR spatially smoothed mask (edge-strength input) from SPOQC_HQTR_REFINEMENT + pattern: "hqtr_output_mask_smoothed_raw" + ontologies: [] +output: + report: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "report": + type: directory + description: HTML/plot QC report directory produced for this step + ontologies: [] + versions_spoqc: + - - "${task.process}": + type: string + description: The process the versions were collected from + - spoqc: + type: string + description: The tool name + - "spoqc --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 + - spoqc: + type: string + description: The tool name + - "spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown": + type: eval + description: The expression to obtain the version of the tool +authors: + - "@heylf" +maintainers: + - "@heylf" diff --git a/modules/local/spoQC/hqtr_bounding_box/tests/main.nf.test b/modules/local/spoQC/hqtr_bounding_box/tests/main.nf.test new file mode 100644 index 00000000..3e18fc3c --- /dev/null +++ b/modules/local/spoQC/hqtr_bounding_box/tests/main.nf.test @@ -0,0 +1,147 @@ +nextflow_process { + + name "Test Process SPOQC_HQTR_BOUNDING_BOX" + script "../main.nf" + process "SPOQC_HQTR_BOUNDING_BOX" + config "./nextflow.config" + + tag "modules" + tag "modules_local" + tag "spoqc" + tag "spoqc/hqtr_bounding_box" + + setup { + run("UNTAR") { + script "modules/nf-core/untar/main.nf" + process { + """ + input[0] = [[], file('https://raw.githubusercontent.com/nf-core/test-datasets/spatialaxe/xenium_bundle.tar.gz', checkIfExists: true)] + """ + } + } + + run("SPATIALDATA_WRITE") { + script "modules/local/spatialdata/write/main.nf" + process { + """ + input[0] = channel.of([ + [id: "test_run_spoqc"], + ]).combine(UNTAR.out.untar.map { it[1] }) + input[1] = "spatialdata_raw" + input[2] = [] + input[3] = "all" + """ + } + } + + run("SPOQC_AMBIENT") { + script "modules/local/spoQC/ambient/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "ambientqc" + """ + } + } + + run("SPOQC_HQTR_METRICES") { + script "modules/local/spoQC/hqtr_metrices/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_metrices" + """ + } + } + + run("SPOQC_HQTR_QV") { + script "modules/local/spoQC/hqtr_qv/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_qv" + """ + } + } + + run("SPOQC_HQTR_AC") { + script "modules/local/spoQC/hqtr_ac/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_ac" + input[2] = SPOQC_AMBIENT.out.tmp.map { it[1] } + """ + } + } + + run("SPOQC_HQTR_CLUSTERING") { + script "modules/local/spoQC/hqtr_clustering/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_clustering" + input[2] = SPOQC_HQTR_METRICES.out.metrices.map { it[1] } + input[3] = SPOQC_HQTR_QV.out.tmp.map { it[1] } + input[4] = SPOQC_HQTR_AC.out.tmp.map { it[1] } + """ + } + } + + run("SPOQC_HQTR_REFINEMENT") { + script "modules/local/spoQC/hqtr_refinement/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_refinement" + input[2] = SPOQC_HQTR_CLUSTERING.out.mask.map { it[1] } + """ + } + } + } + + test("spoqc hqtr_bounding_box") { + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_bounding_box" + input[2] = SPOQC_HQTR_REFINEMENT.out.mask_smoothed.map { it[1] } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + + test("spoqc hqtr_bounding_box stub") { + + options "-stub" + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_bounding_box" + input[2] = SPOQC_HQTR_REFINEMENT.out.mask_smoothed.map { it[1] } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + +} diff --git a/modules/local/spoQC/hqtr_bounding_box/tests/main.nf.test.snap b/modules/local/spoQC/hqtr_bounding_box/tests/main.nf.test.snap new file mode 100644 index 00000000..da28da56 --- /dev/null +++ b/modules/local/spoQC/hqtr_bounding_box/tests/main.nf.test.snap @@ -0,0 +1,34 @@ +{ + "spoqc hqtr_bounding_box": { + "content": [ + [ + [ + "SPOQC_HQTR_BOUNDING_BOX", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T17:58:24.176161046", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + }, + "spoqc hqtr_bounding_box stub": { + "content": [ + [ + [ + "SPOQC_HQTR_BOUNDING_BOX", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T18:00:32.844720971", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + } +} \ No newline at end of file diff --git a/modules/local/spoQC/hqtr_bounding_box/tests/nextflow.config b/modules/local/spoQC/hqtr_bounding_box/tests/nextflow.config new file mode 100644 index 00000000..f8b3a30a --- /dev/null +++ b/modules/local/spoQC/hqtr_bounding_box/tests/nextflow.config @@ -0,0 +1,9 @@ +process { + + resourceLimits = [ + cpus: 4, + memory: '8.GB', + time: '2.h', + ] + +} diff --git a/modules/local/spoQC/hqtr_celltype/main.nf b/modules/local/spoQC/hqtr_celltype/main.nf new file mode 100644 index 00000000..3e5bf467 --- /dev/null +++ b/modules/local/spoQC/hqtr_celltype/main.nf @@ -0,0 +1,62 @@ + +process SPOQC_HQTR_CELLTYPE { + tag "$meta.id" + label 'process_tiny_cpus' + label 'process_high_mem' + label 'process_tiny_time' + label 'spoqc' + + + container "heylf/spoqc:0.0.1" + + input: + tuple val(meta), path(spatialdata, stageAs: "*") + path(annotation, stageAs: "*") + val(step) + path(tmp_general, stageAs: "./spoQC_tmp/generalqc_output_hqcr.parquet") + path(tmp_bubble, stageAs: "./spoQC_tmp/bubbleqc_output_hqcr.parquet") + path(tmp_doublet, stageAs: "./spoQC_tmp/doubletqc_output_hqcr.parquet") + path(tmp_void, stageAs: "./spoQC_tmp/voidqc_output_hqcr.parquet") + path(tmp_cell, stageAs: "./spoQC_tmp/cellqc_output_hqcr.parquet") + path(mask, stageAs: "./spoQC_tmp/hqtr_output_mask_raw") + path(mask_smoothed, stageAs: "./spoQC_tmp/hqtr_output_mask_smoothed_raw") + + output: + tuple val(meta), path("./report/hqtr/hqtr_celltype") , emit: report + tuple val("${task.process}"), val('spoqc'), eval("spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown"), topic: versions, emit: versions_spoqc + + 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("SPOQC_HQTR_CELLTYPE module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + def args = task.ext.args ?: '' + def annotation_arg = annotation ? "-a ${annotation}" : "" + + """ + python3 -m spoqc \\ + -i ${spatialdata} \\ + -o ./ \\ + -t ./spoQC_tmp/ \\ + -n ${task.cpus} \\ + ${annotation_arg} \\ + -s ${step} \\ + --dataset ${meta.id} \\ + ${args} + """ + + stub: + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error("SPOQC_HQTR_CELLTYPE module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + """ + mkdir -p ./report/hqtr/hqtr_celltype + """ + +} diff --git a/modules/local/spoQC/hqtr_celltype/meta.yml b/modules/local/spoQC/hqtr_celltype/meta.yml new file mode 100644 index 00000000..7a27da30 --- /dev/null +++ b/modules/local/spoQC/hqtr_celltype/meta.yml @@ -0,0 +1,107 @@ +name: spoqc_hqtr_celltype +description: | + Run high-quality transcript region (HQTR) cell-type QC, combining the general/bubble/doublet/void/cell QC outputs with the HQTR probability and mask. +keywords: + - spatial transcriptomics + - quality control + - spoqc + - xenium + - hqtr + - cell type +tools: + - "spoqc": + description: | + spoQC is a modular framework for multimodal quality control (QC) of imaging-based spatially resolved transcriptomics (SRT). + homepage: https://github.com/heylf/spoQC + documentation: https://github.com/heylf/spoQC + licence: ["MIT license"] +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - spatialdata: + type: directory + description: Path to the SpatialData zarr store to run QC on + ontologies: [] + - annotation: + type: file + description: | + Optional cell-type annotation file. When empty/absent, spoqc is run without the `-a` flag. + pattern: "*.tsv" + ontologies: [] + - step: + type: string + description: Name of the spoQC step to run, passed to `spoqc -s` + - tmp_general: + type: file + description: General QC parquet output from SPOQC_GENERAL + pattern: "generalqc_output_hqcr.parquet" + ontologies: [] + - tmp_bubble: + type: file + description: Bubble QC parquet output from SPOQC_BUBBLE + pattern: "bubbleqc_output_hqcr.parquet" + ontologies: [] + - tmp_doublet: + type: file + description: Doublet QC parquet output from SPOQC_DOUBLET + pattern: "doubletqc_output_hqcr.parquet" + ontologies: [] + - tmp_void: + type: file + description: Void QC parquet output from SPOQC_VOID + pattern: "voidqc_output_hqcr.parquet" + ontologies: [] + - tmp_cell: + type: file + description: Cell QC parquet output from SPOQC_CELL + pattern: "cellqc_output_hqcr.parquet" + ontologies: [] + - mask: + type: directory + description: HQTR raw probability mask from SPOQC_HQTR_CLUSTERING + pattern: "hqtr_output_mask_raw" + ontologies: [] + - mask_smoothed: + type: directory + description: Smoothed HQTR mask from SPOQC_HQTR_REFINEMENT + pattern: "hqtr_output_mask_smoothed_raw" + ontologies: [] +output: + report: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "report": + type: directory + description: HTML/plot QC report directory produced for this step + ontologies: [] + versions_spoqc: + - - "${task.process}": + type: string + description: The process the versions were collected from + - spoqc: + type: string + description: The tool name + - "spoqc --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 + - spoqc: + type: string + description: The tool name + - "spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown": + type: eval + description: The expression to obtain the version of the tool +authors: + - "@heylf" +maintainers: + - "@heylf" diff --git a/modules/local/spoQC/hqtr_celltype/tests/main.nf.test b/modules/local/spoQC/hqtr_celltype/tests/main.nf.test new file mode 100644 index 00000000..5d07c920 --- /dev/null +++ b/modules/local/spoQC/hqtr_celltype/tests/main.nf.test @@ -0,0 +1,224 @@ +nextflow_process { + + name "Test Process SPOQC_HQTR_CELLTYPE" + script "../main.nf" + process "SPOQC_HQTR_CELLTYPE" + config "./nextflow.config" + + tag "modules" + tag "modules_local" + tag "spoqc" + tag "spoqc/hqtr_celltype" + + setup { + run("UNTAR") { + script "modules/nf-core/untar/main.nf" + process { + """ + input[0] = [[], file('https://raw.githubusercontent.com/nf-core/test-datasets/spatialaxe/xenium_bundle.tar.gz', checkIfExists: true)] + """ + } + } + + run("SPATIALDATA_WRITE") { + script "modules/local/spatialdata/write/main.nf" + process { + """ + input[0] = channel.of([ + [id: "test_run_spoqc"], + ]).combine(UNTAR.out.untar.map { it[1] }) + input[1] = "spatialdata_raw" + input[2] = [] + input[3] = "all" + """ + } + } + + run("SPOQC_ANNOTATION") { + script "modules/local/spoQC/annotation/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = [] + input[2] = "annotation" + """ + } + } + + run("SPOQC_GENERAL") { + script "modules/local/spoQC/general/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "generalqc" + """ + } + } + + run("SPOQC_BUBBLE") { + script "modules/local/spoQC/bubble/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "bubbleqc" + """ + } + } + + run("SPOQC_DOUBLET") { + script "modules/local/spoQC/doublet/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "doubletqc" + """ + } + } + + run("SPOQC_VOID") { + script "modules/local/spoQC/void/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "voidqc" + """ + } + } + + run("SPOQC_CELL") { + script "modules/local/spoQC/cell/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "cellqc" + """ + } + } + + run("SPOQC_AMBIENT") { + script "modules/local/spoQC/ambient/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "ambientqc" + """ + } + } + + run("SPOQC_HQTR_METRICES") { + script "modules/local/spoQC/hqtr_metrices/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_metrices" + """ + } + } + + run("SPOQC_HQTR_QV") { + script "modules/local/spoQC/hqtr_qv/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_qv" + """ + } + } + + run("SPOQC_HQTR_AC") { + script "modules/local/spoQC/hqtr_ac/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_ac" + input[2] = SPOQC_AMBIENT.out.tmp.map { it[1] } + """ + } + } + + run("SPOQC_HQTR_CLUSTERING") { + script "modules/local/spoQC/hqtr_clustering/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_clustering" + input[2] = SPOQC_HQTR_METRICES.out.metrices.map { it[1] } + input[3] = SPOQC_HQTR_QV.out.tmp.map { it[1] } + input[4] = SPOQC_HQTR_AC.out.tmp.map { it[1] } + """ + } + } + + run("SPOQC_HQTR_REFINEMENT") { + script "modules/local/spoQC/hqtr_refinement/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_refinement" + input[2] = SPOQC_HQTR_CLUSTERING.out.mask.map { it[1] } + """ + } + } + } + + test("spoqc hqtr_celltype") { + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "hqtr_celltype" + input[3] = SPOQC_GENERAL.out.tmp.map { it[1] } + input[4] = SPOQC_BUBBLE.out.tmp.map { it[1] } + input[5] = SPOQC_DOUBLET.out.tmp.map { it[1] } + input[6] = SPOQC_VOID.out.tmp.map { it[1] } + input[7] = SPOQC_CELL.out.tmp.map { it[1] } + input[8] = SPOQC_HQTR_CLUSTERING.out.mask.map { it[1] } + input[9] = SPOQC_HQTR_REFINEMENT.out.mask_smoothed.map { it[1] } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + + test("spoqc hqtr_celltype stub") { + + options "-stub" + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "hqtr_celltype" + input[3] = SPOQC_GENERAL.out.tmp.map { it[1] } + input[4] = SPOQC_BUBBLE.out.tmp.map { it[1] } + input[5] = SPOQC_DOUBLET.out.tmp.map { it[1] } + input[6] = SPOQC_VOID.out.tmp.map { it[1] } + input[7] = SPOQC_CELL.out.tmp.map { it[1] } + input[8] = SPOQC_HQTR_CLUSTERING.out.mask.map { it[1] } + input[9] = SPOQC_HQTR_REFINEMENT.out.mask_smoothed.map { it[1] } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + +} diff --git a/modules/local/spoQC/hqtr_celltype/tests/main.nf.test.snap b/modules/local/spoQC/hqtr_celltype/tests/main.nf.test.snap new file mode 100644 index 00000000..4bc93785 --- /dev/null +++ b/modules/local/spoQC/hqtr_celltype/tests/main.nf.test.snap @@ -0,0 +1,34 @@ +{ + "spoqc hqtr_celltype stub": { + "content": [ + [ + [ + "SPOQC_HQTR_CELLTYPE", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T18:18:11.089528578", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + }, + "spoqc hqtr_celltype": { + "content": [ + [ + [ + "SPOQC_HQTR_CELLTYPE", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T18:14:54.24578931", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + } +} \ No newline at end of file diff --git a/modules/local/spoQC/hqtr_celltype/tests/nextflow.config b/modules/local/spoQC/hqtr_celltype/tests/nextflow.config new file mode 100644 index 00000000..f8b3a30a --- /dev/null +++ b/modules/local/spoQC/hqtr_celltype/tests/nextflow.config @@ -0,0 +1,9 @@ +process { + + resourceLimits = [ + cpus: 4, + memory: '8.GB', + time: '2.h', + ] + +} diff --git a/modules/local/spoQC/hqtr_clustering/main.nf b/modules/local/spoQC/hqtr_clustering/main.nf new file mode 100644 index 00000000..d06c50b9 --- /dev/null +++ b/modules/local/spoQC/hqtr_clustering/main.nf @@ -0,0 +1,57 @@ + +process SPOQC_HQTR_CLUSTERING { + tag "$meta.id" + label 'process_xl_cpus' + label 'process_xl_mem' + label 'process_xl_time' + label 'spoqc' + + + container "heylf/spoqc:0.0.1" + + input: + tuple val(meta), path(spatialdata, stageAs: "*") + val(step) + path(metrices, stageAs: "spoQC_tmp/metrices/hqtr") + path(qv, stageAs: "spoQC_tmp/*") + path(ac, stageAs: "spoQC_tmp/*") + + output: + tuple val(meta), path("report/hqtr/hqtr_clustering") , emit: report + tuple val(meta), path("spoQC_tmp/hqtr_output_mask_raw") , emit: mask + tuple val("${task.process}"), val('spoqc'), eval("spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown"), topic: versions, emit: versions_spoqc + + 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("SPOQC_HQTR_CLUSTERING module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + def args = task.ext.args ?: '' + + """ + python3 -m spoqc \\ + -i ${spatialdata} \\ + -o ./ \\ + -t ./spoQC_tmp/ \\ + -n ${task.cpus} \\ + -s ${step} \\ + --dataset ${meta.id} \\ + ${args} + """ + + stub: + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error("SPOQC_HQTR_CLUSTERING module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + """ + mkdir -p report/hqtr/hqtr_clustering + mkdir -p spoQC_tmp/hqtr_output_mask_raw + """ + +} diff --git a/modules/local/spoQC/hqtr_clustering/meta.yml b/modules/local/spoQC/hqtr_clustering/meta.yml new file mode 100644 index 00000000..74956272 --- /dev/null +++ b/modules/local/spoQC/hqtr_clustering/meta.yml @@ -0,0 +1,92 @@ +name: spoqc_hqtr_clustering +description: | + Cluster high-quality transcript region (HQTR) metrics, quality-value and ambient-contamination probabilities into a raw mask. +keywords: + - spatial transcriptomics + - quality control + - spoqc + - xenium + - hqtr + - clustering +tools: + - "spoqc": + description: | + spoQC is a modular framework for multimodal quality control (QC) of imaging-based spatially resolved transcriptomics (SRT). + homepage: https://github.com/heylf/spoQC + documentation: https://github.com/heylf/spoQC + licence: ["MIT license"] +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - spatialdata: + type: directory + description: Path to the SpatialData zarr store to run QC on + ontologies: [] + - step: + type: string + description: Name of the spoQC step to run, passed to `spoqc -s` + - metrices: + type: directory + description: HQTR metrics directory from SPOQC_HQTR_METRICES + pattern: "spoQC_tmp/metrices/hqtr" + ontologies: [] + - qv: + type: directory + description: HQTR quality-value probability output from SPOQC_HQTR_QV + pattern: "spoQC_tmp/*" + ontologies: [] + - ac: + type: directory + description: HQTR ambient-contamination probability output from SPOQC_HQTR_AC + pattern: "spoQC_tmp/*" + ontologies: [] +output: + report: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "report": + type: directory + description: HTML/plot QC report directory produced for this step + ontologies: [] + mask: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "hqtr_output_mask_raw": + type: directory + description: HQTR raw probability mask + pattern: "hqtr_output_mask_raw" + ontologies: [] + versions_spoqc: + - - "${task.process}": + type: string + description: The process the versions were collected from + - spoqc: + type: string + description: The tool name + - "spoqc --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 + - spoqc: + type: string + description: The tool name + - "spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown": + type: eval + description: The expression to obtain the version of the tool +authors: + - "@heylf" +maintainers: + - "@heylf" diff --git a/modules/local/spoQC/hqtr_clustering/tests/main.nf.test b/modules/local/spoQC/hqtr_clustering/tests/main.nf.test new file mode 100644 index 00000000..8316f01c --- /dev/null +++ b/modules/local/spoQC/hqtr_clustering/tests/main.nf.test @@ -0,0 +1,129 @@ +nextflow_process { + + name "Test Process SPOQC_HQTR_CLUSTERING" + script "../main.nf" + process "SPOQC_HQTR_CLUSTERING" + config "./nextflow.config" + + tag "modules" + tag "modules_local" + tag "spoqc" + tag "spoqc/hqtr_clustering" + + setup { + run("UNTAR") { + script "modules/nf-core/untar/main.nf" + process { + """ + input[0] = [[], file('https://raw.githubusercontent.com/nf-core/test-datasets/spatialaxe/xenium_bundle.tar.gz', checkIfExists: true)] + """ + } + } + + run("SPATIALDATA_WRITE") { + script "modules/local/spatialdata/write/main.nf" + process { + """ + input[0] = channel.of([ + [id: "test_run_spoqc"], + ]).combine(UNTAR.out.untar.map { it[1] }) + input[1] = "spatialdata_raw" + input[2] = [] + input[3] = "all" + """ + } + } + + run("SPOQC_AMBIENT") { + script "modules/local/spoQC/ambient/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "ambientqc" + """ + } + } + + run("SPOQC_HQTR_METRICES") { + script "modules/local/spoQC/hqtr_metrices/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_metrices" + """ + } + } + + run("SPOQC_HQTR_QV") { + script "modules/local/spoQC/hqtr_qv/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_qv" + """ + } + } + + run("SPOQC_HQTR_AC") { + script "modules/local/spoQC/hqtr_ac/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_ac" + input[2] = SPOQC_AMBIENT.out.tmp.map { it[1] } + """ + } + } + } + + test("spoqc hqtr_clustering") { + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_clustering" + input[2] = SPOQC_HQTR_METRICES.out.metrices.map { it[1] } + input[3] = SPOQC_HQTR_QV.out.tmp.map { it[1] } + input[4] = SPOQC_HQTR_AC.out.tmp.map { it[1] } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert file(process.out.mask[0][1]).isDirectory() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + + test("spoqc hqtr_clustering stub") { + + options "-stub" + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_clustering" + input[2] = SPOQC_HQTR_METRICES.out.metrices.map { it[1] } + input[3] = SPOQC_HQTR_QV.out.tmp.map { it[1] } + input[4] = SPOQC_HQTR_AC.out.tmp.map { it[1] } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert file(process.out.mask[0][1]).isDirectory() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + +} diff --git a/modules/local/spoQC/hqtr_clustering/tests/main.nf.test.snap b/modules/local/spoQC/hqtr_clustering/tests/main.nf.test.snap new file mode 100644 index 00000000..071f9679 --- /dev/null +++ b/modules/local/spoQC/hqtr_clustering/tests/main.nf.test.snap @@ -0,0 +1,34 @@ +{ + "spoqc hqtr_clustering stub": { + "content": [ + [ + [ + "SPOQC_HQTR_CLUSTERING", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T18:36:21.878073172", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + }, + "spoqc hqtr_clustering": { + "content": [ + [ + [ + "SPOQC_HQTR_CLUSTERING", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T18:34:54.523078946", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + } +} \ No newline at end of file diff --git a/modules/local/spoQC/hqtr_clustering/tests/nextflow.config b/modules/local/spoQC/hqtr_clustering/tests/nextflow.config new file mode 100644 index 00000000..f8b3a30a --- /dev/null +++ b/modules/local/spoQC/hqtr_clustering/tests/nextflow.config @@ -0,0 +1,9 @@ +process { + + resourceLimits = [ + cpus: 4, + memory: '8.GB', + time: '2.h', + ] + +} diff --git a/modules/local/spoQC/hqtr_metrices/main.nf b/modules/local/spoQC/hqtr_metrices/main.nf new file mode 100644 index 00000000..bcf9eafd --- /dev/null +++ b/modules/local/spoQC/hqtr_metrices/main.nf @@ -0,0 +1,53 @@ + +process SPOQC_HQTR_METRICES { + tag "$meta.id" + label 'process_xl_cpus' + label 'process_xl_mem' + label 'process_mid_time' + label 'spoqc' + + + container "heylf/spoqc:0.0.1" + + input: + tuple val(meta), path(spatialdata, stageAs: "*") + val(step) + + output: + tuple val(meta), path("report/hqtr/hqtr_metrices") , emit: report + tuple val(meta), path("spoQC_tmp/metrices/hqtr") , emit: metrices + tuple val("${task.process}"), val('spoqc'), eval("spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown"), topic: versions, emit: versions_spoqc + + 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("SPOQC_HQTR_METRICES module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + def args = task.ext.args ?: '' + + """ + python3 -m spoqc \\ + -i ${spatialdata} \\ + -o ./ \\ + -t ./spoQC_tmp/ \\ + -n ${task.cpus} \\ + -s ${step} \\ + --dataset ${meta.id} \\ + ${args} + """ + + stub: + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error("SPOQC_HQTR_METRICES module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + """ + mkdir -p report/hqtr/hqtr_metrices + mkdir -p spoQC_tmp/metrices/hqtr + """ +} diff --git a/modules/local/spoQC/hqtr_metrices/meta.yml b/modules/local/spoQC/hqtr_metrices/meta.yml new file mode 100644 index 00000000..bbd2ca7f --- /dev/null +++ b/modules/local/spoQC/hqtr_metrices/meta.yml @@ -0,0 +1,77 @@ +name: spoqc_hqtr_metrices +description: | + Compute high-quality transcript region (HQTR) metrics for a SpatialData object. +keywords: + - spatial transcriptomics + - quality control + - spoqc + - xenium + - hqtr + - high quality transcript region +tools: + - "spoqc": + description: | + spoQC is a modular framework for multimodal quality control (QC) of imaging-based spatially resolved transcriptomics (SRT). + homepage: https://github.com/heylf/spoQC + documentation: https://github.com/heylf/spoQC + licence: ["MIT license"] +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - spatialdata: + type: directory + description: Path to the SpatialData zarr store to run QC on + ontologies: [] + - step: + type: string + description: Name of the spoQC step to run, passed to `spoqc -s` +output: + report: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "report": + type: directory + description: HTML/plot QC report directory produced for this step + ontologies: [] + metrices: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "spoQC_tmp/metrices/hqtr": + type: directory + description: HQTR metrics directory + pattern: "spoQC_tmp/metrices/hqtr" + ontologies: [] + versions_spoqc: + - - "${task.process}": + type: string + description: The process the versions were collected from + - spoqc: + type: string + description: The tool name + - "spoqc --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 + - spoqc: + type: string + description: The tool name + - "spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown": + type: eval + description: The expression to obtain the version of the tool +authors: + - "@heylf" +maintainers: + - "@heylf" diff --git a/modules/local/spoQC/hqtr_metrices/tests/main.nf.test b/modules/local/spoQC/hqtr_metrices/tests/main.nf.test new file mode 100644 index 00000000..282fdb56 --- /dev/null +++ b/modules/local/spoQC/hqtr_metrices/tests/main.nf.test @@ -0,0 +1,82 @@ +nextflow_process { + + name "Test Process SPOQC_HQTR_METRICES" + script "../main.nf" + process "SPOQC_HQTR_METRICES" + config "./nextflow.config" + + tag "modules" + tag "modules_local" + tag "spoqc" + tag "spoqc/hqtr_metrices" + + setup { + run("UNTAR") { + script "modules/nf-core/untar/main.nf" + process { + """ + input[0] = [[], file('https://raw.githubusercontent.com/nf-core/test-datasets/spatialaxe/xenium_bundle.tar.gz', checkIfExists: true)] + """ + } + } + + run("SPATIALDATA_WRITE") { + script "modules/local/spatialdata/write/main.nf" + process { + """ + input[0] = channel.of([ + [id: "test_run_spoqc"], + ]).combine(UNTAR.out.untar.map { it[1] }) + input[1] = "spatialdata_raw" + input[2] = [] + input[3] = "all" + """ + } + } + } + + test("spoqc hqtr_metrices") { + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_metrices" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert file(process.out.metrices[0][1]).isDirectory() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + + test("spoqc hqtr_metrices stub") { + + options "-stub" + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_metrices" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert file(process.out.metrices[0][1]).isDirectory() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + +} diff --git a/modules/local/spoQC/hqtr_metrices/tests/main.nf.test.snap b/modules/local/spoQC/hqtr_metrices/tests/main.nf.test.snap new file mode 100644 index 00000000..92fe8993 --- /dev/null +++ b/modules/local/spoQC/hqtr_metrices/tests/main.nf.test.snap @@ -0,0 +1,34 @@ +{ + "spoqc hqtr_metrices": { + "content": [ + [ + [ + "SPOQC_HQTR_METRICES", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T21:44:38.17989478", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + }, + "spoqc hqtr_metrices stub": { + "content": [ + [ + [ + "SPOQC_HQTR_METRICES", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-12T21:45:02.974116381", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + } +} \ No newline at end of file diff --git a/modules/local/spoQC/hqtr_metrices/tests/nextflow.config b/modules/local/spoQC/hqtr_metrices/tests/nextflow.config new file mode 100644 index 00000000..f8b3a30a --- /dev/null +++ b/modules/local/spoQC/hqtr_metrices/tests/nextflow.config @@ -0,0 +1,9 @@ +process { + + resourceLimits = [ + cpus: 4, + memory: '8.GB', + time: '2.h', + ] + +} diff --git a/modules/local/spoQC/hqtr_qv/main.nf b/modules/local/spoQC/hqtr_qv/main.nf new file mode 100644 index 00000000..c748f50d --- /dev/null +++ b/modules/local/spoQC/hqtr_qv/main.nf @@ -0,0 +1,54 @@ + +process SPOQC_HQTR_QV { + tag "$meta.id" + label 'process_xl_cpus' + label 'process_xl_mem' + label 'process_mid_time' + label 'spoqc' + + + container "heylf/spoqc:0.0.1" + + input: + tuple val(meta), path(spatialdata, stageAs: "*") + val(step) + + output: + tuple val(meta), path("./report/hqtr/hqtr_qv") , emit: report + tuple val(meta), path("./spoQC_tmp/hqtr_output_qv_prob") , emit: tmp + tuple val("${task.process}"), val('spoqc'), eval("spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown"), topic: versions, emit: versions_spoqc + + 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("SPOQC_HQTR_QV module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + def args = task.ext.args ?: '' + + """ + python3 -m spoqc \\ + -i ${spatialdata} \\ + -o ./ \\ + -t ./spoQC_tmp/ \\ + -n ${task.cpus} \\ + -s ${step} \\ + --dataset ${meta.id} \\ + ${args} + """ + + stub: + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error("SPOQC_HQTR_QV module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + """ + mkdir -p ./report/hqtr/hqtr_qv + mkdir -p ./spoQC_tmp/hqtr_output_qv_prob + """ + +} diff --git a/modules/local/spoQC/hqtr_qv/meta.yml b/modules/local/spoQC/hqtr_qv/meta.yml new file mode 100644 index 00000000..59a5a4a7 --- /dev/null +++ b/modules/local/spoQC/hqtr_qv/meta.yml @@ -0,0 +1,77 @@ +name: spoqc_hqtr_qv +description: | + Estimate quality-value probability for high-quality transcript regions (HQTR). +keywords: + - spatial transcriptomics + - quality control + - spoqc + - xenium + - hqtr + - quality value +tools: + - "spoqc": + description: | + spoQC is a modular framework for multimodal quality control (QC) of imaging-based spatially resolved transcriptomics (SRT). + homepage: https://github.com/heylf/spoQC + documentation: https://github.com/heylf/spoQC + licence: ["MIT license"] +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - spatialdata: + type: directory + description: Path to the SpatialData zarr store to run QC on + ontologies: [] + - step: + type: string + description: Name of the spoQC step to run, passed to `spoqc -s` +output: + report: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "report": + type: directory + description: HTML/plot QC report directory produced for this step + ontologies: [] + tmp: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "hqtr_output_qv_prob": + type: directory + description: HQTR quality-value probability output + pattern: "hqtr_output_qv_prob" + ontologies: [] + versions_spoqc: + - - "${task.process}": + type: string + description: The process the versions were collected from + - spoqc: + type: string + description: The tool name + - "spoqc --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 + - spoqc: + type: string + description: The tool name + - "spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown": + type: eval + description: The expression to obtain the version of the tool +authors: + - "@heylf" +maintainers: + - "@heylf" diff --git a/modules/local/spoQC/hqtr_qv/tests/main.nf.test b/modules/local/spoQC/hqtr_qv/tests/main.nf.test new file mode 100644 index 00000000..52326fcd --- /dev/null +++ b/modules/local/spoQC/hqtr_qv/tests/main.nf.test @@ -0,0 +1,82 @@ +nextflow_process { + + name "Test Process SPOQC_HQTR_QV" + script "../main.nf" + process "SPOQC_HQTR_QV" + config "./nextflow.config" + + tag "modules" + tag "modules_local" + tag "spoqc" + tag "spoqc/hqtr_qv" + + setup { + run("UNTAR") { + script "modules/nf-core/untar/main.nf" + process { + """ + input[0] = [[], file('https://raw.githubusercontent.com/nf-core/test-datasets/spatialaxe/xenium_bundle.tar.gz', checkIfExists: true)] + """ + } + } + + run("SPATIALDATA_WRITE") { + script "modules/local/spatialdata/write/main.nf" + process { + """ + input[0] = channel.of([ + [id: "test_run_spoqc"], + ]).combine(UNTAR.out.untar.map { it[1] }) + input[1] = "spatialdata_raw" + input[2] = [] + input[3] = "all" + """ + } + } + } + + test("spoqc hqtr_qv") { + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_qv" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert file(process.out.tmp[0][1]).isDirectory() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + + test("spoqc hqtr_qv stub") { + + options "-stub" + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_qv" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert file(process.out.tmp[0][1]).isDirectory() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + +} diff --git a/modules/local/spoQC/hqtr_qv/tests/main.nf.test.snap b/modules/local/spoQC/hqtr_qv/tests/main.nf.test.snap new file mode 100644 index 00000000..2b1e0971 --- /dev/null +++ b/modules/local/spoQC/hqtr_qv/tests/main.nf.test.snap @@ -0,0 +1,34 @@ +{ + "spoqc hqtr_qv stub": { + "content": [ + [ + [ + "SPOQC_HQTR_QV", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-13T09:21:54.759747419", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + }, + "spoqc hqtr_qv": { + "content": [ + [ + [ + "SPOQC_HQTR_QV", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-13T09:21:29.448859605", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + } +} \ No newline at end of file diff --git a/modules/local/spoQC/hqtr_qv/tests/nextflow.config b/modules/local/spoQC/hqtr_qv/tests/nextflow.config new file mode 100644 index 00000000..f8b3a30a --- /dev/null +++ b/modules/local/spoQC/hqtr_qv/tests/nextflow.config @@ -0,0 +1,9 @@ +process { + + resourceLimits = [ + cpus: 4, + memory: '8.GB', + time: '2.h', + ] + +} diff --git a/modules/local/spoQC/hqtr_refinement/main.nf b/modules/local/spoQC/hqtr_refinement/main.nf new file mode 100644 index 00000000..3901cfc4 --- /dev/null +++ b/modules/local/spoQC/hqtr_refinement/main.nf @@ -0,0 +1,54 @@ +process SPOQC_HQTR_REFINEMENT { + tag "$meta.id" + label 'process_xl_cpus' + label 'process_xl_mem' + label 'process_mid_time' + label 'spoqc' + + + container "heylf/spoqc:0.0.1" + + input: + tuple val(meta), path(spatialdata, stageAs: "*") + val(step) + path(mask, stageAs: "./spoQC_tmp/hqtr_output_mask_raw") + + output: + tuple val(meta), path("./report/hqtr/hqtr_refinement") , emit: report + tuple val(meta), path("./spoQC_tmp/hqtr_output_mask_smoothed_raw") , emit: mask_smoothed + tuple val("${task.process}"), val('spoqc'), eval("spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown"), topic: versions, emit: versions_spoqc + + 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("SPOQC_HQTR_REFINEMENT module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + def args = task.ext.args ?: '' + + """ + python3 -m spoqc \\ + -i ${spatialdata} \\ + -o ./ \\ + -t ./spoQC_tmp/ \\ + -n ${task.cpus} \\ + -s ${step} \\ + --dataset ${meta.id} \\ + ${args} + """ + + stub: + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error("SPOQC_HQTR_REFINEMENT module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + """ + mkdir -p ./report/hqtr/hqtr_refinement + mkdir -p ./spoQC_tmp/hqtr_output_mask_smoothed_raw + """ + +} diff --git a/modules/local/spoQC/hqtr_refinement/meta.yml b/modules/local/spoQC/hqtr_refinement/meta.yml new file mode 100644 index 00000000..4a73ef61 --- /dev/null +++ b/modules/local/spoQC/hqtr_refinement/meta.yml @@ -0,0 +1,82 @@ +name: spoqc_hqtr_refinement +description: | + Refine the high-quality transcript region (HQTR) probability mask into a smoothed mask. +keywords: + - spatial transcriptomics + - quality control + - spoqc + - xenium + - hqtr + - mask refinement +tools: + - "spoqc": + description: | + spoQC is a modular framework for multimodal quality control (QC) of imaging-based spatially resolved transcriptomics (SRT). + homepage: https://github.com/heylf/spoQC + documentation: https://github.com/heylf/spoQC + licence: ["MIT license"] +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - spatialdata: + type: directory + description: Path to the SpatialData zarr store to run QC on + ontologies: [] + - step: + type: string + description: Name of the spoQC step to run, passed to `spoqc -s` + - mask: + type: directory + description: HQTR raw mask from SPOQC_HQTR_CLUSTERING + pattern: "hqtr_output_mask_raw" + ontologies: [] +output: + report: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "report": + type: directory + description: HTML/plot QC report directory produced for this step + ontologies: [] + mask_smoothed: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "hqtr_output_mask_smoothed_raw": + type: directory + description: Smoothed HQTR mask + pattern: "hqtr_output_mask_smoothed_raw" + ontologies: [] + versions_spoqc: + - - "${task.process}": + type: string + description: The process the versions were collected from + - spoqc: + type: string + description: The tool name + - "spoqc --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 + - spoqc: + type: string + description: The tool name + - "spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown": + type: eval + description: The expression to obtain the version of the tool +authors: + - "@heylf" +maintainers: + - "@heylf" diff --git a/modules/local/spoQC/hqtr_refinement/tests/main.nf.test b/modules/local/spoQC/hqtr_refinement/tests/main.nf.test new file mode 100644 index 00000000..ace62148 --- /dev/null +++ b/modules/local/spoQC/hqtr_refinement/tests/main.nf.test @@ -0,0 +1,138 @@ +nextflow_process { + + name "Test Process SPOQC_HQTR_REFINEMENT" + script "../main.nf" + process "SPOQC_HQTR_REFINEMENT" + config "./nextflow.config" + + tag "modules" + tag "modules_local" + tag "spoqc" + tag "spoqc/hqtr_refinement" + + setup { + run("UNTAR") { + script "modules/nf-core/untar/main.nf" + process { + """ + input[0] = [[], file('https://raw.githubusercontent.com/nf-core/test-datasets/spatialaxe/xenium_bundle.tar.gz', checkIfExists: true)] + """ + } + } + + run("SPATIALDATA_WRITE") { + script "modules/local/spatialdata/write/main.nf" + process { + """ + input[0] = channel.of([ + [id: "test_run_spoqc"], + ]).combine(UNTAR.out.untar.map { it[1] }) + input[1] = "spatialdata_raw" + input[2] = [] + input[3] = "all" + """ + } + } + + run("SPOQC_AMBIENT") { + script "modules/local/spoQC/ambient/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "ambientqc" + """ + } + } + + run("SPOQC_HQTR_METRICES") { + script "modules/local/spoQC/hqtr_metrices/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_metrices" + """ + } + } + + run("SPOQC_HQTR_QV") { + script "modules/local/spoQC/hqtr_qv/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_qv" + """ + } + } + + run("SPOQC_HQTR_AC") { + script "modules/local/spoQC/hqtr_ac/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_ac" + input[2] = SPOQC_AMBIENT.out.tmp.map { it[1] } + """ + } + } + + run("SPOQC_HQTR_CLUSTERING") { + script "modules/local/spoQC/hqtr_clustering/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_clustering" + input[2] = SPOQC_HQTR_METRICES.out.metrices.map { it[1] } + input[3] = SPOQC_HQTR_QV.out.tmp.map { it[1] } + input[4] = SPOQC_HQTR_AC.out.tmp.map { it[1] } + """ + } + } + } + + test("spoqc hqtr_refinement") { + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_refinement" + input[2] = SPOQC_HQTR_CLUSTERING.out.mask.map { it[1] } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert file(process.out.mask_smoothed[0][1]).isDirectory() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + + test("spoqc hqtr_refinement stub") { + + options "-stub" + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "hqtr_refinement" + input[2] = SPOQC_HQTR_CLUSTERING.out.mask.map { it[1] } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert file(process.out.mask_smoothed[0][1]).isDirectory() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + +} diff --git a/modules/local/spoQC/hqtr_refinement/tests/main.nf.test.snap b/modules/local/spoQC/hqtr_refinement/tests/main.nf.test.snap new file mode 100644 index 00000000..21d52235 --- /dev/null +++ b/modules/local/spoQC/hqtr_refinement/tests/main.nf.test.snap @@ -0,0 +1,34 @@ +{ + "spoqc hqtr_refinement stub": { + "content": [ + [ + [ + "SPOQC_HQTR_REFINEMENT", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-13T09:39:25.497874543", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + }, + "spoqc hqtr_refinement": { + "content": [ + [ + [ + "SPOQC_HQTR_REFINEMENT", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-13T09:36:53.11938098", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + } +} \ No newline at end of file diff --git a/modules/local/spoQC/hqtr_refinement/tests/nextflow.config b/modules/local/spoQC/hqtr_refinement/tests/nextflow.config new file mode 100644 index 00000000..f8b3a30a --- /dev/null +++ b/modules/local/spoQC/hqtr_refinement/tests/nextflow.config @@ -0,0 +1,9 @@ +process { + + resourceLimits = [ + cpus: 4, + memory: '8.GB', + time: '2.h', + ] + +} diff --git a/modules/local/spoQC/marker/main.nf b/modules/local/spoQC/marker/main.nf new file mode 100644 index 00000000..9924f8e5 --- /dev/null +++ b/modules/local/spoQC/marker/main.nf @@ -0,0 +1,54 @@ +process SPOQC_MARKER { + tag "$meta.id" + label 'process_tiny_cpus' + label 'process_low_mem' + label 'process_tiny_time' + label 'spoqc' + + + container "heylf/spoqc:0.0.1" + + input: + tuple val(meta), path(spatialdata, stageAs: "*") + path(annotation, stageAs: "*") + val(step) + + output: + tuple val(meta), path("./report/markerqc") , emit: report + tuple val("${task.process}"), val('spoqc'), eval("spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown"), topic: versions, emit: versions_spoqc + + 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("SPOQC_MARKER module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + def args = task.ext.args ?: '' + def annotation_arg = annotation ? "-a ${annotation}" : "" + + """ + python3 -m spoqc \\ + -i ${spatialdata} \\ + -o ./ \\ + -t ./spoQC_tmp/ \\ + -n ${task.cpus} \\ + ${annotation_arg} \\ + -s ${step} \\ + --dataset ${meta.id} \\ + ${args} + """ + + stub: + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error("SPOQC_MARKER module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + """ + mkdir -p ./report/markerqc + """ + +} diff --git a/modules/local/spoQC/marker/meta.yml b/modules/local/spoQC/marker/meta.yml new file mode 100644 index 00000000..b395b72e --- /dev/null +++ b/modules/local/spoQC/marker/meta.yml @@ -0,0 +1,73 @@ +name: spoqc_marker +description: | + Run marker-gene QC checks on a SpatialData object. + + Currently not wired into the `SPOQC` subworkflow (the call is commented out in `subworkflows/local/spoqc/main.nf`). +keywords: + - spatial transcriptomics + - quality control + - spoqc + - xenium + - marker gene +tools: + - "spoqc": + description: | + spoQC is a modular framework for multimodal quality control (QC) of imaging-based spatially resolved transcriptomics (SRT). + homepage: https://github.com/heylf/spoQC + documentation: https://github.com/heylf/spoQC + licence: ["MIT license"] +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - spatialdata: + type: directory + description: Path to the SpatialData zarr store to run QC on + ontologies: [] + - annotation: + type: file + description: | + Optional cell-type annotation file. When empty/absent, spoqc is run without the `-a` flag. + pattern: "*.tsv" + ontologies: [] + - step: + type: string + description: Name of the spoQC step to run, passed to `spoqc -s` +output: + report: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "report": + type: directory + description: HTML/plot QC report directory produced for this step + ontologies: [] + versions_spoqc: + - - "${task.process}": + type: string + description: The process the versions were collected from + - spoqc: + type: string + description: The tool name + - "spoqc --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 + - spoqc: + type: string + description: The tool name + - "spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown": + type: eval + description: The expression to obtain the version of the tool +authors: + - "@heylf" +maintainers: + - "@heylf" diff --git a/modules/local/spoQC/marker/tests/main.nf.test b/modules/local/spoQC/marker/tests/main.nf.test new file mode 100644 index 00000000..cf1ca572 --- /dev/null +++ b/modules/local/spoQC/marker/tests/main.nf.test @@ -0,0 +1,93 @@ +nextflow_process { + + name "Test Process SPOQC_MARKER" + script "../main.nf" + process "SPOQC_MARKER" + config "./nextflow.config" + + tag "modules" + tag "modules_local" + tag "spoqc" + tag "spoqc/marker" + + setup { + run("UNTAR") { + script "modules/nf-core/untar/main.nf" + process { + """ + input[0] = [[], file('https://raw.githubusercontent.com/nf-core/test-datasets/spatialaxe/xenium_bundle.tar.gz', checkIfExists: true)] + """ + } + } + + run("SPATIALDATA_WRITE") { + script "modules/local/spatialdata/write/main.nf" + process { + """ + input[0] = channel.of([ + [id: "test_run_spoqc"], + ]).combine(UNTAR.out.untar.map { it[1] }) + input[1] = "spatialdata_raw" + input[2] = [] + input[3] = "all" + """ + } + } + + run("SPOQC_ANNOTATION") { + script "modules/local/spoQC/annotation/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = [] + input[2] = "annotation" + """ + } + } + } + + test("spoqc marker") { + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "markerqc" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + + test("spoqc marker stub") { + + options "-stub" + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "markerqc" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + +} diff --git a/modules/local/spoQC/marker/tests/main.nf.test.snap b/modules/local/spoQC/marker/tests/main.nf.test.snap new file mode 100644 index 00000000..43ba96e9 --- /dev/null +++ b/modules/local/spoQC/marker/tests/main.nf.test.snap @@ -0,0 +1,34 @@ +{ + "spoqc marker stub": { + "content": [ + [ + [ + "SPOQC_MARKER", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-13T09:42:51.122429373", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + }, + "spoqc marker": { + "content": [ + [ + [ + "SPOQC_MARKER", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-13T09:41:51.207027596", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + } +} \ No newline at end of file diff --git a/modules/local/spoQC/marker/tests/nextflow.config b/modules/local/spoQC/marker/tests/nextflow.config new file mode 100644 index 00000000..f8b3a30a --- /dev/null +++ b/modules/local/spoQC/marker/tests/nextflow.config @@ -0,0 +1,9 @@ +process { + + resourceLimits = [ + cpus: 4, + memory: '8.GB', + time: '2.h', + ] + +} diff --git a/modules/local/spoQC/model/main.nf b/modules/local/spoQC/model/main.nf new file mode 100644 index 00000000..0b8f8761 --- /dev/null +++ b/modules/local/spoQC/model/main.nf @@ -0,0 +1,51 @@ +process SPOQC_MODEL { + tag "$meta.id" + label 'process_tiny_cpus' + label 'process_low_mem' + label 'process_tiny_time' + label 'spoqc' + + + container "heylf/spoqc:0.0.1" + + input: + tuple val(meta), path(spatialdata, stageAs: "*") + val(step) + + output: + tuple val(meta), path("./report/modelqc") , emit: report + tuple val("${task.process}"), val('spoqc'), eval("spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown"), topic: versions, emit: versions_spoqc + + 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("SPOQC_MODEL module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + def args = task.ext.args ?: '' + + """ + python3 -m spoqc \\ + -i ${spatialdata} \\ + -o ./ \\ + -t ./spoQC_tmp/ \\ + -n ${task.cpus} \\ + -s ${step} \\ + --dataset ${meta.id} \\ + ${args} + """ + + stub: + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error("SPOQC_MODEL module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + """ + mkdir -p ./report/modelqc + """ + +} diff --git a/modules/local/spoQC/model/meta.yml b/modules/local/spoQC/model/meta.yml new file mode 100644 index 00000000..fef67853 --- /dev/null +++ b/modules/local/spoQC/model/meta.yml @@ -0,0 +1,65 @@ +name: spoqc_model +description: | + Run model-based QC scoring on a SpatialData object. +keywords: + - spatial transcriptomics + - quality control + - spoqc + - xenium + - model qc +tools: + - "spoqc": + description: | + spoQC is a modular framework for multimodal quality control (QC) of imaging-based spatially resolved transcriptomics (SRT). + homepage: https://github.com/heylf/spoQC + documentation: https://github.com/heylf/spoQC + licence: ["MIT license"] +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - spatialdata: + type: directory + description: Path to the SpatialData zarr store to run QC on + ontologies: [] + - step: + type: string + description: Name of the spoQC step to run, passed to `spoqc -s` +output: + report: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "report": + type: directory + description: HTML/plot QC report directory produced for this step + ontologies: [] + versions_spoqc: + - - "${task.process}": + type: string + description: The process the versions were collected from + - spoqc: + type: string + description: The tool name + - "spoqc --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 + - spoqc: + type: string + description: The tool name + - "spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown": + type: eval + description: The expression to obtain the version of the tool +authors: + - "@heylf" +maintainers: + - "@heylf" diff --git a/modules/local/spoQC/model/tests/main.nf.test b/modules/local/spoQC/model/tests/main.nf.test new file mode 100644 index 00000000..72c5b539 --- /dev/null +++ b/modules/local/spoQC/model/tests/main.nf.test @@ -0,0 +1,80 @@ +nextflow_process { + + name "Test Process SPOQC_MODEL" + script "../main.nf" + process "SPOQC_MODEL" + config "./nextflow.config" + + tag "modules" + tag "modules_local" + tag "spoqc" + tag "spoqc/model" + + setup { + run("UNTAR") { + script "modules/nf-core/untar/main.nf" + process { + """ + input[0] = [[], file('https://raw.githubusercontent.com/nf-core/test-datasets/spatialaxe/xenium_bundle.tar.gz', checkIfExists: true)] + """ + } + } + + run("SPATIALDATA_WRITE") { + script "modules/local/spatialdata/write/main.nf" + process { + """ + input[0] = channel.of([ + [id: "test_run_spoqc"], + ]).combine(UNTAR.out.untar.map { it[1] }) + input[1] = "spatialdata_raw" + input[2] = [] + input[3] = "all" + """ + } + } + } + + test("spoqc model") { + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "modelqc" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + + test("spoqc model stub") { + + options "-stub" + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "modelqc" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + +} diff --git a/modules/local/spoQC/model/tests/main.nf.test.snap b/modules/local/spoQC/model/tests/main.nf.test.snap new file mode 100644 index 00000000..00b142b2 --- /dev/null +++ b/modules/local/spoQC/model/tests/main.nf.test.snap @@ -0,0 +1,34 @@ +{ + "spoqc model": { + "content": [ + [ + [ + "SPOQC_MODEL", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-13T09:44:49.066274033", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + }, + "spoqc model stub": { + "content": [ + [ + [ + "SPOQC_MODEL", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-13T09:45:27.560109648", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + } +} \ No newline at end of file diff --git a/modules/local/spoQC/model/tests/nextflow.config b/modules/local/spoQC/model/tests/nextflow.config new file mode 100644 index 00000000..f8b3a30a --- /dev/null +++ b/modules/local/spoQC/model/tests/nextflow.config @@ -0,0 +1,9 @@ +process { + + resourceLimits = [ + cpus: 4, + memory: '8.GB', + time: '2.h', + ] + +} diff --git a/modules/local/spoQC/transcript/main.nf b/modules/local/spoQC/transcript/main.nf new file mode 100644 index 00000000..f3fada35 --- /dev/null +++ b/modules/local/spoQC/transcript/main.nf @@ -0,0 +1,55 @@ + +process SPOQC_TRANSCRIPT { + tag "$meta.id" + label 'process_tiny_cpus' + label 'process_low_mem' + label 'process_tiny_time' + label 'spoqc' + + + container "heylf/spoqc:0.0.1" + + input: + tuple val(meta), path(spatialdata, stageAs: "*") + path(annotation, stageAs: "*") + val(step) + + output: + tuple val(meta), path("./report/transcriptqc") , emit: report + tuple val("${task.process}"), val('spoqc'), eval("spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown"), topic: versions, emit: versions_spoqc + + 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("SPOQC_TRANSCRIPT module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + def args = task.ext.args ?: '' + def annotation_arg = annotation ? "-a ${annotation}" : "" + + """ + python3 -m spoqc \\ + -i ${spatialdata} \\ + -o ./ \\ + -t ./spoQC_tmp/ \\ + -n ${task.cpus} \\ + ${annotation_arg} \\ + -s ${step} \\ + --dataset ${meta.id} \\ + ${args} + """ + + stub: + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error("SPOQC_TRANSCRIPT module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + """ + mkdir -p ./report/transcriptqc + """ + +} diff --git a/modules/local/spoQC/transcript/meta.yml b/modules/local/spoQC/transcript/meta.yml new file mode 100644 index 00000000..bd547d56 --- /dev/null +++ b/modules/local/spoQC/transcript/meta.yml @@ -0,0 +1,71 @@ +name: spoqc_transcript +description: | + Run transcript-level QC against a reference gene panel for a SpatialData object. +keywords: + - spatial transcriptomics + - quality control + - spoqc + - xenium + - transcript qc +tools: + - "spoqc": + description: | + spoQC is a modular framework for multimodal quality control (QC) of imaging-based spatially resolved transcriptomics (SRT). + homepage: https://github.com/heylf/spoQC + documentation: https://github.com/heylf/spoQC + licence: ["MIT license"] +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - spatialdata: + type: directory + description: Path to the SpatialData zarr store to run QC on + ontologies: [] + - annotation: + type: file + description: | + Optional cell-type annotation file. When empty/absent, spoqc is run without the `-a` flag. + pattern: "*.tsv" + ontologies: [] + - step: + type: string + description: Name of the spoQC step to run, passed to `spoqc -s` +output: + report: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "report": + type: directory + description: HTML/plot QC report directory produced for this step + ontologies: [] + versions_spoqc: + - - "${task.process}": + type: string + description: The process the versions were collected from + - spoqc: + type: string + description: The tool name + - "spoqc --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 + - spoqc: + type: string + description: The tool name + - "spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown": + type: eval + description: The expression to obtain the version of the tool +authors: + - "@heylf" +maintainers: + - "@heylf" diff --git a/modules/local/spoQC/transcript/tests/main.nf.test b/modules/local/spoQC/transcript/tests/main.nf.test new file mode 100644 index 00000000..c27d1556 --- /dev/null +++ b/modules/local/spoQC/transcript/tests/main.nf.test @@ -0,0 +1,93 @@ +nextflow_process { + + name "Test Process SPOQC_TRANSCRIPT" + script "../main.nf" + process "SPOQC_TRANSCRIPT" + config "./nextflow.config" + + tag "modules" + tag "modules_local" + tag "spoqc" + tag "spoqc/transcript" + + setup { + run("UNTAR") { + script "modules/nf-core/untar/main.nf" + process { + """ + input[0] = [[], file('https://raw.githubusercontent.com/nf-core/test-datasets/spatialaxe/xenium_bundle.tar.gz', checkIfExists: true)] + """ + } + } + + run("SPATIALDATA_WRITE") { + script "modules/local/spatialdata/write/main.nf" + process { + """ + input[0] = channel.of([ + [id: "test_run_spoqc"], + ]).combine(UNTAR.out.untar.map { it[1] }) + input[1] = "spatialdata_raw" + input[2] = [] + input[3] = "all" + """ + } + } + + run("SPOQC_ANNOTATION") { + script "modules/local/spoQC/annotation/main.nf" + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = [] + input[2] = "annotation" + """ + } + } + } + + test("spoqc transcript") { + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "transcriptqc" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + + test("spoqc transcript stub") { + + options "-stub" + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = SPOQC_ANNOTATION.out.annotation.map { it[1] } + input[2] = "transcriptqc" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + +} diff --git a/modules/local/spoQC/transcript/tests/main.nf.test.snap b/modules/local/spoQC/transcript/tests/main.nf.test.snap new file mode 100644 index 00000000..1aeeb2de --- /dev/null +++ b/modules/local/spoQC/transcript/tests/main.nf.test.snap @@ -0,0 +1,34 @@ +{ + "spoqc transcript stub": { + "content": [ + [ + [ + "SPOQC_TRANSCRIPT", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-13T09:48:49.609160615", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + }, + "spoqc transcript": { + "content": [ + [ + [ + "SPOQC_TRANSCRIPT", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-13T09:47:45.886086129", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + } +} \ No newline at end of file diff --git a/modules/local/spoQC/transcript/tests/nextflow.config b/modules/local/spoQC/transcript/tests/nextflow.config new file mode 100644 index 00000000..f8b3a30a --- /dev/null +++ b/modules/local/spoQC/transcript/tests/nextflow.config @@ -0,0 +1,9 @@ +process { + + resourceLimits = [ + cpus: 4, + memory: '8.GB', + time: '2.h', + ] + +} diff --git a/modules/local/spoQC/void/main.nf b/modules/local/spoQC/void/main.nf new file mode 100644 index 00000000..38b857dd --- /dev/null +++ b/modules/local/spoQC/void/main.nf @@ -0,0 +1,55 @@ + +process SPOQC_VOID { + tag "$meta.id" + label 'process_xl_cpus' + label 'process_low_mem' + label 'process_low_time' + label 'spoqc' + + + container "heylf/spoqc:0.0.1" + + input: + tuple val(meta), path(spatialdata, stageAs: "*") + val(step) + + output: + tuple val(meta), path("./report/voidqc") , emit: report + tuple val(meta), path("./spoQC_tmp/voidqc_output_hqcr.parquet") , emit: tmp + tuple val("${task.process}"), val('spoqc'), eval("spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown"), topic: versions, emit: versions_spoqc + + 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("SPOQC_VOID module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + def args = task.ext.args ?: '' + + """ + python3 -m spoqc \\ + -i ${spatialdata} \\ + -o ./ \\ + -t ./spoQC_tmp/ \\ + -n ${task.cpus} \\ + -s ${step} \\ + --dataset ${meta.id} \\ + ${args} + """ + + stub: + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error("SPOQC_VOID module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + """ + mkdir -p ./report/voidqc + mkdir -p ./spoQC_tmp + touch ./spoQC_tmp/voidqc_output_hqcr.parquet + """ + +} diff --git a/modules/local/spoQC/void/meta.yml b/modules/local/spoQC/void/meta.yml new file mode 100644 index 00000000..9db75919 --- /dev/null +++ b/modules/local/spoQC/void/meta.yml @@ -0,0 +1,77 @@ +name: spoqc_void +description: | + Run void/empty-region QC checks on a SpatialData object. +keywords: + - spatial transcriptomics + - quality control + - spoqc + - xenium + - void detection +tools: + - "spoqc": + description: | + spoQC is a modular framework for multimodal quality control (QC) of imaging-based spatially resolved transcriptomics (SRT). + homepage: https://github.com/heylf/spoQC + documentation: https://github.com/heylf/spoQC + licence: ["MIT license"] +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - spatialdata: + type: directory + description: Path to the SpatialData zarr store to run QC on + ontologies: [] + - step: + type: string + description: Name of the spoQC step to run, passed to `spoqc -s` +output: + report: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "report": + type: directory + description: HTML/plot QC report directory produced for this step + ontologies: [] + tmp: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "voidqc_output_hqcr.parquet": + type: file + description: | + Per-cell void QC parquet output, consumed by the HQCR identification/cell-type and downstream analysis steps. + pattern: "voidqc_output_hqcr.parquet" + ontologies: [] + versions_spoqc: + - - "${task.process}": + type: string + description: The process the versions were collected from + - spoqc: + type: string + description: The tool name + - "spoqc --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 + - spoqc: + type: string + description: The tool name + - "spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown": + type: eval + description: The expression to obtain the version of the tool +authors: + - "@heylf" +maintainers: + - "@heylf" diff --git a/modules/local/spoQC/void/tests/main.nf.test b/modules/local/spoQC/void/tests/main.nf.test new file mode 100644 index 00000000..2bc24cdd --- /dev/null +++ b/modules/local/spoQC/void/tests/main.nf.test @@ -0,0 +1,82 @@ +nextflow_process { + + name "Test Process SPOQC_VOID" + script "../main.nf" + process "SPOQC_VOID" + config "./nextflow.config" + + tag "modules" + tag "modules_local" + tag "spoqc" + tag "spoqc/void" + + setup { + run("UNTAR") { + script "modules/nf-core/untar/main.nf" + process { + """ + input[0] = [[], file('https://raw.githubusercontent.com/nf-core/test-datasets/spatialaxe/xenium_bundle.tar.gz', checkIfExists: true)] + """ + } + } + + run("SPATIALDATA_WRITE") { + script "modules/local/spatialdata/write/main.nf" + process { + """ + input[0] = channel.of([ + [id: "test_run_spoqc"], + ]).combine(UNTAR.out.untar.map { it[1] }) + input[1] = "spatialdata_raw" + input[2] = [] + input[3] = "all" + """ + } + } + } + + test("spoqc void") { + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "voidqc" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert file(process.out.tmp[0][1]).exists() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + + test("spoqc void stub") { + + options "-stub" + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "voidqc" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert file(process.out.tmp[0][1]).exists() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + +} diff --git a/modules/local/spoQC/void/tests/main.nf.test.snap b/modules/local/spoQC/void/tests/main.nf.test.snap new file mode 100644 index 00000000..d55e7ff4 --- /dev/null +++ b/modules/local/spoQC/void/tests/main.nf.test.snap @@ -0,0 +1,34 @@ +{ + "spoqc void stub": { + "content": [ + [ + [ + "SPOQC_VOID", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-13T09:50:55.405731827", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + }, + "spoqc void": { + "content": [ + [ + [ + "SPOQC_VOID", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-13T09:50:17.518303623", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + } +} \ No newline at end of file diff --git a/modules/local/spoQC/void/tests/nextflow.config b/modules/local/spoQC/void/tests/nextflow.config new file mode 100644 index 00000000..f8b3a30a --- /dev/null +++ b/modules/local/spoQC/void/tests/nextflow.config @@ -0,0 +1,9 @@ +process { + + resourceLimits = [ + cpus: 4, + memory: '8.GB', + time: '2.h', + ] + +} diff --git a/modules/local/spoQC/whole_slide/main.nf b/modules/local/spoQC/whole_slide/main.nf new file mode 100644 index 00000000..5c76e483 --- /dev/null +++ b/modules/local/spoQC/whole_slide/main.nf @@ -0,0 +1,52 @@ + +process SPOQC_WHOLE_SLIDE { + tag "$meta.id" + label 'process_tiny_cpus' + label 'process_low_mem' + label 'process_tiny_time' + label 'spoqc' + + + container "heylf/spoqc:0.0.1" + + input: + tuple val(meta), path(spatialdata, stageAs: "*") + val(step) + + output: + tuple val(meta), path("./report/whole_slide_qc") , emit: report + tuple val("${task.process}"), val('spoqc'), eval("spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown"), topic: versions, emit: versions_spoqc + + 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("SPOQC_WHOLE_SLIDE module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + def args = task.ext.args ?: '' + + """ + python3 -m spoqc \\ + -i ${spatialdata} \\ + -o ./ \\ + -t ./spoQC_tmp/ \\ + -n ${task.cpus} \\ + -s ${step} \\ + --dataset ${meta.id} \\ + ${args} + """ + + stub: + // Exit if running this module with -profile conda / -profile mamba + if (workflow.profile.tokenize(',').intersect(['conda', 'mamba']).size() >= 1) { + error("SPOQC_WHOLE_SLIDE module does not support Conda. Please use Docker / Singularity / Podman instead.") + } + + """ + mkdir -p ./report/whole_slide_qc + """ + +} diff --git a/modules/local/spoQC/whole_slide/meta.yml b/modules/local/spoQC/whole_slide/meta.yml new file mode 100644 index 00000000..2615d48c --- /dev/null +++ b/modules/local/spoQC/whole_slide/meta.yml @@ -0,0 +1,65 @@ +name: spoqc_whole_slide +description: | + Generate a whole-slide QC overview report for a SpatialData object. +keywords: + - spatial transcriptomics + - quality control + - spoqc + - xenium + - whole slide +tools: + - "spoqc": + description: | + spoQC is a modular framework for multimodal quality control (QC) of imaging-based spatially resolved transcriptomics (SRT). + homepage: https://github.com/heylf/spoQC + documentation: https://github.com/heylf/spoQC + licence: ["MIT license"] +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - spatialdata: + type: directory + description: Path to the SpatialData zarr store to run QC on + ontologies: [] + - step: + type: string + description: Name of the spoQC step to run, passed to `spoqc -s` +output: + report: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "report": + type: directory + description: HTML/plot QC report directory produced for this step + ontologies: [] + versions_spoqc: + - - "${task.process}": + type: string + description: The process the versions were collected from + - spoqc: + type: string + description: The tool name + - "spoqc --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 + - spoqc: + type: string + description: The tool name + - "spoqc --version 2>&1 | grep -oP '\\d+\\.\\d+\\.\\d+' || echo unknown": + type: eval + description: The expression to obtain the version of the tool +authors: + - "@heylf" +maintainers: + - "@heylf" diff --git a/modules/local/spoQC/whole_slide/tests/main.nf.test b/modules/local/spoQC/whole_slide/tests/main.nf.test new file mode 100644 index 00000000..b1cf441e --- /dev/null +++ b/modules/local/spoQC/whole_slide/tests/main.nf.test @@ -0,0 +1,80 @@ +nextflow_process { + + name "Test Process SPOQC_WHOLE_SLIDE" + script "../main.nf" + process "SPOQC_WHOLE_SLIDE" + config "./nextflow.config" + + tag "modules" + tag "modules_local" + tag "spoqc" + tag "spoqc/whole_slide" + + setup { + run("UNTAR") { + script "modules/nf-core/untar/main.nf" + process { + """ + input[0] = [[], file('https://raw.githubusercontent.com/nf-core/test-datasets/spatialaxe/xenium_bundle.tar.gz', checkIfExists: true)] + """ + } + } + + run("SPATIALDATA_WRITE") { + script "modules/local/spatialdata/write/main.nf" + process { + """ + input[0] = channel.of([ + [id: "test_run_spoqc"], + ]).combine(UNTAR.out.untar.map { it[1] }) + input[1] = "spatialdata_raw" + input[2] = [] + input[3] = "all" + """ + } + } + } + + test("spoqc whole_slide") { + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "whole_slide_qc" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + + test("spoqc whole_slide stub") { + + options "-stub" + + when { + process { + """ + input[0] = SPATIALDATA_WRITE.out.spatialdata + input[1] = "whole_slide_qc" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report[0][1]).isDirectory() }, + { assert snapshot(process.out.versions_spoqc).match() } + ) + } + } + +} diff --git a/modules/local/spoQC/whole_slide/tests/main.nf.test.snap b/modules/local/spoQC/whole_slide/tests/main.nf.test.snap new file mode 100644 index 00000000..469d219d --- /dev/null +++ b/modules/local/spoQC/whole_slide/tests/main.nf.test.snap @@ -0,0 +1,34 @@ +{ + "spoqc whole_slide stub": { + "content": [ + [ + [ + "SPOQC_WHOLE_SLIDE", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-13T09:53:13.863446176", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + }, + "spoqc whole_slide": { + "content": [ + [ + [ + "SPOQC_WHOLE_SLIDE", + "spoqc", + "0.1.0" + ] + ] + ], + "timestamp": "2026-08-13T09:52:35.665231657", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + } +} \ No newline at end of file diff --git a/modules/local/spoQC/whole_slide/tests/nextflow.config b/modules/local/spoQC/whole_slide/tests/nextflow.config new file mode 100644 index 00000000..f8b3a30a --- /dev/null +++ b/modules/local/spoQC/whole_slide/tests/nextflow.config @@ -0,0 +1,9 @@ +process { + + resourceLimits = [ + cpus: 4, + memory: '8.GB', + time: '2.h', + ] + +} diff --git a/modules/nf-core/unzip/main.nf b/modules/nf-core/unzip/main.nf index d9417fb3..d0dc6f31 100644 --- a/modules/nf-core/unzip/main.nf +++ b/modules/nf-core/unzip/main.nf @@ -3,16 +3,16 @@ process UNZIP { label 'process_single' conda "${moduleDir}/environment.yml" - container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? 'https://depot.galaxyproject.org/singularity/p7zip:16.02' : - 'biocontainers/p7zip:16.02' }" + 'quay.io/biocontainers/p7zip:16.02' }" input: tuple val(meta), path(archive) output: tuple val(meta), path("${prefix}/"), emit: unzipped_archive - path "versions.yml" , emit: versions + tuple val("${task.process}"), val('7za'), eval("7za 2>&1 | sed -n '2s/^.* \\([0-9.]*\\) .*/\\1/p'"), topic: versions, emit: versions_7za when: task.ext.when == null || task.ext.when @@ -27,24 +27,49 @@ process UNZIP { -o"${prefix}"/ \\ $args \\ $archive - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - 7za: \$(echo \$(7za --help) | sed 's/.*p7zip Version //; s/(.*//') - END_VERSIONS """ stub: if ( archive instanceof List && archive.name.size > 1 ) { error "[UNZIP] error: 7za only accepts a single archive as input. Please check module input." } prefix = task.ext.prefix ?: ( meta.id ? "${meta.id}" : archive.baseName) """ - mkdir -p "${prefix}/morphology_focus" - touch "${prefix}/transcripts.parquet" - touch "${prefix}/morphology_focus/morphology_focus_0000.ome.tif" - - cat <<-END_VERSIONS > versions.yml - "${task.process}": - 7za: \$(echo \$(7za --help) | sed 's/.*p7zip Version //; s/(.*//') - END_VERSIONS + mkdir "${prefix}" + + ## Emit a valid empty file. nf-test's snapshot md5 path decompresses .gz files, + ## so a 0-byte .gz (from `touch`) throws EOF in GZIPInputStream and the snapshot + ## falls back to dumping non-deterministic File metadata (freeSpace, etc). + ## `: | gzip -n` produces a 20-byte deterministic empty gzip that md5s consistently. + emit_stub() { + local f="\$1" + if [[ "\$f" == *.gz ]]; then + : | gzip -n > "\$f" + else + touch "\$f" + fi + } + + ## Dry-run listing the archive contents (7za is the only archiver guaranteed to be + ## present) and mirror its structure under prefix, matching the unstripped layout + ## that `7za x` produces in the real script block. + 7za l -slt "${archive}" | awk ' + BEGIN{RS=""; FS="\\n"} + \$1=="----------"{started=1} + started{ + path=""; isdir=0 + for(i=1;i<=NF;i++){ + if (\$i ~ /^Path = /) path=substr(\$i,8) + else if (\$i ~ /^Folder = \\+/) isdir=1 + } + if (path!="") print (isdir?"D":"F") "\\t" path + } + ' | while IFS=\$'\\t' read -r type path; do + [ -z "\${path}" ] && continue + if [ "\${type}" = "D" ]; then + mkdir -p "${prefix}/\${path}" + else + mkdir -p "${prefix}/\$(dirname "\${path}")" + emit_stub "${prefix}/\${path}" + fi + done """ } diff --git a/modules/nf-core/unzip/meta.yml b/modules/nf-core/unzip/meta.yml index ba1eb912..1c645556 100644 --- a/modules/nf-core/unzip/meta.yml +++ b/modules/nf-core/unzip/meta.yml @@ -7,12 +7,13 @@ keywords: - archiving tools: - unzip: - description: p7zip is a quick port of 7z.exe and 7za.exe (command line version - of 7zip, see www.7-zip.org) for Unix. + description: p7zip is a quick port of 7z.exe and 7za.exe (command line + version of 7zip, see www.7-zip.org) for Unix. homepage: https://sourceforge.net/projects/p7zip/ documentation: https://sourceforge.net/projects/p7zip/ tool_dev_url: https://sourceforge.net/projects/p7zip" - licence: ["LGPL-2.1-or-later"] + licence: + - "LGPL-2.1-or-later" identifier: "" input: - - meta: @@ -25,7 +26,7 @@ input: description: ZIP file pattern: "*.zip" ontologies: - - edam: http://edamontology.org/format_3987 # ZIP format + - edam: http://edamontology.org/format_3987 output: unzipped_archive: - - meta: @@ -37,13 +38,27 @@ output: type: directory description: Directory contents of the unzipped archive pattern: "${archive.baseName}/" + versions_7za: + - - ${task.process}: + type: string + description: The name of the process + - 7za: + type: string + description: The name of the tool + - 7za 2>&1 | sed -n '2s/^.* \([0-9.]*\) .*/\1/p': + 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 + - 7za: + type: string + description: The name of the tool + - 7za 2>&1 | sed -n '2s/^.* \([0-9.]*\) .*/\1/p': + type: eval + description: The expression to obtain the version of the tool authors: - "@jfy133" maintainers: diff --git a/modules/nf-core/unzip/tests/main.nf.test b/modules/nf-core/unzip/tests/main.nf.test index 238b68d8..6e5997d2 100644 --- a/modules/nf-core/unzip/tests/main.nf.test +++ b/modules/nf-core/unzip/tests/main.nf.test @@ -24,7 +24,7 @@ nextflow_process { then { assertAll( { assert process.success }, - { assert snapshot(process.out).match() } + { assert snapshot(sanitizeOutput(process.out)).match() } ) } } @@ -47,7 +47,7 @@ nextflow_process { then { assertAll( { assert process.success }, - { assert snapshot(process.out).match() } + { assert snapshot(sanitizeOutput(process.out)).match() } ) } } diff --git a/modules/nf-core/unzip/tests/main.nf.test.snap b/modules/nf-core/unzip/tests/main.nf.test.snap index cdd2ab16..3d178111 100644 --- a/modules/nf-core/unzip/tests/main.nf.test.snap +++ b/modules/nf-core/unzip/tests/main.nf.test.snap @@ -2,7 +2,7 @@ "generic [tar] [tar_gz] stub": { "content": [ { - "0": [ + "unzipped_archive": [ [ { "id": "hello" @@ -12,34 +12,25 @@ ] ] ], - "1": [ - "versions.yml:md5,52c55ce814e8bc9edc5a6c625ed794b8" - ], - "unzipped_archive": [ + "versions_7za": [ [ - { - "id": "hello" - }, - [ - - ] + "UNZIP", + "7za", + "16.02" ] - ], - "versions": [ - "versions.yml:md5,52c55ce814e8bc9edc5a6c625ed794b8" ] } ], + "timestamp": "2026-06-15T16:55:32.454288336", "meta": { - "nf-test": "0.8.4", - "nextflow": "24.04.2" - }, - "timestamp": "2024-06-30T19:16:37.11550986" + "nf-test": "0.9.5", + "nextflow": "26.04.0" + } }, "generic [tar] [tar_gz]": { "content": [ { - "0": [ + "unzipped_archive": [ [ { "id": "hello" @@ -49,28 +40,19 @@ ] ] ], - "1": [ - "versions.yml:md5,52c55ce814e8bc9edc5a6c625ed794b8" - ], - "unzipped_archive": [ + "versions_7za": [ [ - { - "id": "hello" - }, - [ - "hello.tar:md5,80c66db79a773bc87b3346035ff9593e" - ] + "UNZIP", + "7za", + "16.02" ] - ], - "versions": [ - "versions.yml:md5,52c55ce814e8bc9edc5a6c625ed794b8" ] } ], + "timestamp": "2026-06-15T16:55:26.181807504", "meta": { - "nf-test": "0.8.4", - "nextflow": "24.04.2" - }, - "timestamp": "2024-06-30T19:16:25.120242571" + "nf-test": "0.9.5", + "nextflow": "26.04.0" + } } } \ No newline at end of file diff --git a/nextflow.config b/nextflow.config index 3f91e590..66d98351 100644 --- a/nextflow.config +++ b/nextflow.config @@ -112,6 +112,7 @@ params { // 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 ) + spoqc = false // whether to run spoqc // utility modules csplit_x_bins = 2 // number of tiles along the x axis (total number of bins is product of x_bins * y_bins) @@ -384,14 +385,14 @@ manifest { description = """A pipeline for spatialomics 10x Xenium In Situ data.""" mainScript = 'main.nf' defaultBranch = 'master' - nextflowVersion = '!>=25.04.0' + nextflowVersion = '!>=26.04.6' version = '1.0.1dev' doi = '' } // Nextflow plugins plugins { - id 'nf-schema@2.5.1' // Validation of pipeline parameters and creation of an input channel from a sample sheet + id 'nf-schema@2.7.2' // Validation of pipeline parameters and creation of an input channel from a sample sheet } validation { diff --git a/nextflow_schema.json b/nextflow_schema.json index 58a47276..8ba05233 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -129,8 +129,7 @@ }, "offtarget_probe_tracking": { "type": "boolean", - "description": "Whether to run the off-target probe tracking.", - "default": false + "description": "Whether to run the off-target probe tracking." }, "segmentation_refinement": { "type": "boolean", @@ -146,7 +145,8 @@ }, "cell_segmentation_only": { "type": "boolean", - "description": "Whether to only run nucleus segmentation." + "description": "Whether to only run nucleus segmentation.", + "default": true }, "nucleus_segmentation_only": { "type": "boolean", @@ -155,7 +155,7 @@ "expansion_distance": { "type": "integer", "default": 5, - "description": "Nuclei boundary expansion distance in µm. Default: 5 (Min: 0, Max: 15 if either boundary-stain or interior-stain are enabled and 100 if nucleus-expansion only)" + "description": "Nuclei boundary expansion distance in \u00b5m. Default: 5 (Min: 0, Max: 15 if either boundary-stain or interior-stain are enabled and 100 if nucleus-expansion only)" }, "dapi_filter": { "type": "integer", @@ -174,7 +174,6 @@ }, "use_gpu": { "type": "boolean", - "default": false, "description": "Enable GPU acceleration (set automatically by the gpu profile)." }, "gpu_queue": { @@ -187,7 +186,6 @@ }, "cellpose_downscale": { "type": "boolean", - "default": false, "description": "Pre-downscale morphology image to avoid Cellpose OOM on large images." }, "sharpen_tiff": { @@ -256,8 +254,7 @@ }, "filter_transcripts": { "type": "boolean", - "description": "Whether to filter the transcripts.parquet file before running Baysor segmentation.", - "default": false + "description": "Whether to filter the transcripts.parquet file before running Baysor segmentation." }, "baysor_scale": { "type": "integer", @@ -325,7 +322,8 @@ }, "max_x": { "type": "number", - "description": "only keep transcripts whose x-coordinate is less than specified limit, if no limit is specified, the default value will retain all transcripts since Xenium slide is <24000 microns in x and y (default: 24000.0)" + "description": "only keep transcripts whose x-coordinate is less than specified limit, if no limit is specified, the default value will retain all transcripts since Xenium slide is <24000 microns in x and y (default: 24000.0)", + "default": 24000 }, "min_y": { "type": "number", @@ -333,12 +331,12 @@ }, "max_y": { "type": "number", - "description": "only keep transcripts whose y-coordinate is less than specified limit, if no limit is specified, the default value will retain all transcripts since Xenium slide is <24000 microns in x and y (default: 24000.0)" + "description": "only keep transcripts whose y-coordinate is less than specified limit, if no limit is specified, the default value will retain all transcripts since Xenium slide is <24000 microns in x and y (default: 24000.0)", + "default": 24000 }, "tiling": { "type": "boolean", - "description": "Enable tiled segmentation for large datasets. Divides transcripts into overlapping patches, runs segmentation in parallel per patch, then stitches results.", - "default": false + "description": "Enable tiled segmentation for large datasets. Divides transcripts into overlapping patches, runs segmentation in parallel per patch, then stitches results." }, "patch_grid": { "type": "string", @@ -358,12 +356,12 @@ "patch_filter_iqr_multiplier": { "type": "number", "description": "IQR multiplier for empirical cell size filtering during stitching.", - "default": 3.0 + "default": 3 }, "patch_filter_z_threshold": { "type": "number", "description": "Z-score threshold for distribution-based cell size filtering during stitching.", - "default": 4.0 + "default": 4 }, "csplit_x_bins": { "type": "integer", @@ -407,13 +405,11 @@ }, "cc_analysis": { "type": "boolean", - "description": "Whether to use connected components for grouping transcripts without direct nucleus association", - "default": false + "description": "Whether to use connected components for grouping transcripts without direct nucleus association" }, "buffer_samples": { "type": "boolean", - "description": "Process only one sample at a time from a multi-sample samplesheet.", - "default": false + "description": "Process only one sample at a time from a multi-sample samplesheet." }, "buffer_size": { "type": "integer", @@ -422,6 +418,17 @@ } } }, + "qc_options": { + "title": "QC options", + "type": "object", + "description": "", + "default": "", + "properties": { + "spoqc": { + "type": "boolean" + } + } + }, "institutional_config_options": { "title": "Institutional config options", "type": "object", @@ -598,6 +605,9 @@ { "$ref": "#/$defs/segmentation_options" }, + { + "$ref": "#/$defs/qc_options" + }, { "$ref": "#/$defs/institutional_config_options" }, diff --git a/ro-crate-metadata.json b/ro-crate-metadata.json index 42e4dcd5..03eac954 100644 --- a/ro-crate-metadata.json +++ b/ro-crate-metadata.json @@ -23,7 +23,7 @@ "@type": "Dataset", "creativeWorkStatus": "InProgress", "datePublished": "2026-08-06T14:03:08+00:00", - "description": "

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

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

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

\n\n[![Open in GitHub Codespaces](https://img.shields.io/badge/Open_In_GitHub_Codespaces-black?labelColor=grey&logo=github)](https://github.com/codespaces/new/nf-core/spatialaxe)\n[![GitHub Actions CI Status](https://github.com/nf-core/spatialaxe/actions/workflows/nf-test.yml/badge.svg)](https://github.com/nf-core/spatialaxe/actions/workflows/nf-test.yml)\n[![GitHub Actions Linting Status](https://github.com/nf-core/spatialaxe/actions/workflows/linting.yml/badge.svg)](https://github.com/nf-core/spatialaxe/actions/workflows/linting.yml)[![AWS CI](https://img.shields.io/badge/CI%20tests-full%20size-FF9900?labelColor=000000&logo=Amazon%20AWS)](https://nf-co.re/spatialaxe/results)[![Cite with Zenodo](http://img.shields.io/badge/DOI-10.5281/zenodo.20733817-1073c8?labelColor=000000)](https://doi.org/10.5281/zenodo.20733817)\n[![nf-test](https://img.shields.io/badge/unit_tests-nf--test-337ab7.svg)](https://www.nf-test.com)\n\n[![Nextflow](https://img.shields.io/badge/version-%E2%89%A525.04.0-green?style=flat&logo=nextflow&logoColor=white&color=%230DC09D&link=https%3A%2F%2Fnextflow.io)](https://www.nextflow.io/)\n[![nf-core template version](https://img.shields.io/badge/nf--core_template-3.4.1-green?style=flat&logo=nfcore&logoColor=white&color=%2324B064&link=https%3A%2F%2Fnf-co.re)](https://github.com/nf-core/tools/releases/tag/3.4.1)\n[![run with docker](https://img.shields.io/badge/run%20with-docker-0db7ed?labelColor=000000&logo=docker)](https://www.docker.com/)\n[![run with singularity](https://img.shields.io/badge/run%20with-singularity-1d355c.svg?labelColor=000000)](https://sylabs.io/docs/)\n[![Launch on Seqera Platform](https://img.shields.io/badge/Launch%20%F0%9F%9A%80-Seqera%20Platform-%234256e7)](https://cloud.seqera.io/launch?pipeline=https://github.com/nf-core/spatialaxe)\n\n[![Get help on Slack](http://img.shields.io/badge/slack-nf--core%20%23spatialaxe-4A154B?labelColor=000000&logo=slack)](https://nfcore.slack.com/channels/spatialaxe)[![Follow on Bluesky](https://img.shields.io/badge/bluesky-%40nf__core-1185fe?labelColor=000000&logo=bluesky)](https://bsky.app/profile/nf-co.re)[![Follow on Mastodon](https://img.shields.io/badge/mastodon-nf__core-6364ff?labelColor=FFFFFF&logo=mastodon)](https://mstdn.science/@nf_core)[![Watch on YouTube](http://img.shields.io/badge/youtube-nf--core-FF0000?labelColor=000000&logo=youtube)](https://www.youtube.com/c/nf-core)\n\n## Introduction\n\n**nf-core/spatialaxe** is a bioinformatics best-practice processing and quality control pipeline for Xenium (and soon Atera) data. The current plan for the pipeline implementation is shown in the metromap below. **The pipeline is under active developement and changes might occure frequently**.\n\n![nf-core/spatialaxe-metromap](docs/images/spatialaxe-metromap.png)\n\n> [!NOTE]\n> We are currently extending the pipeline for the [10x Atera system](https://www.10xgenomics.com/platforms/atera).\n\n## Tools supported\n\nThe pipeline supports the following tools:\n\n- Segmenation methods:\n - [Baysor](https://doi.org/10.1038/s41587-021-01044-w)\n - [Cellpose](https://doi.org/10.1038/s41592-020-01018-x)\n - [Xenium ranger (XR)](https://www.10xgenomics.com/support/software/xenium-ranger/latest)\n - [StarDist](https://doi.org/10.48550/arXiv.2203.02284)\n- Segmentation free methods:\n - [Ficture](https://doi.org/10.1038/s41592-024-02415-2)\n - [Baysor](https://doi.org/10.1038/s41587-021-01044-w)\n- Transcript assignment methods:\n - [Segger](https://doi.org/10.1101/2025.03.14.643160)\n - [Proseg](https://doi.org/10.1038/s41592-025-02697-0)\n- Utility methods:\n - [SpatialData](https://doi.org/10.1038/s41592-024-02212-x)\n - [Baysor](https://doi.org/10.1038/s41587-021-01044-w)\n- QC methods:\n - [MultiQC Xenium Extra Plugin](https://github.com/MultiQC/xenium-extra)\n - [OPT](https://github.com/JEFworks-Lab/off-target-probe-tracker)\n - [spoQC](https://github.com/heylf/spoQC)\n\n## Usage\n\nOn release, automated continuous integration tests run the pipeline on a full-sized dataset on the AWS cloud infrastructure. This ensures that the pipeline runs on AWS, has sensible resource allocation defaults set to run on real-world datasets, and permits the persistent storage of results to benchmark between pipeline releases and other analysis sources. The results obtained from the full-sized test can be viewed on the [nf-core website](https://nf-co.re/spatialaxe/results).\n\n> [!NOTE]\n> The pipeline does not support conda currently. We are working on it.\n\n## Quick Start\n\n`samplesheet.csv`:\n\n```csv\nsample,bundle,image\ntest_sample,/path/to/xenium-bundle,/path/to/morphology.ome.tif\n```\n\nNow, you can run the pipeline using:\n\n### Run image-based segmentation mode
\n\n`CELLPOSE -> BAYSOR -> XR-IMPORT_SEGMENTATION -> SPATIALDATA -> QC`\n\n```bash\nnextflow run nf-core/spatialaxe \\\n -profile \\\n --input samplesheet.csv \\\n --outdir \\\n --mode \n```\n\n### Run coordinate-based segmentation mode
\n\n`PROSEG -> PROSEG2BAYSOR -> XR-IMPORT_SEGMENTATION -> SPATIALDATA -> QC`\n\n```bash\nnextflow run nf-core/spatialaxe \\\n -profile \\\n --input samplesheet.csv \\\n --outdir \\\n --mode coordinate\n```\n\n### Run segfree mode
\n\n`BAYSOR_SEGFREE`\n\n```bash\nnextflow run nf-core/spatialaxe \\\n -profile \\\n --input samplesheet.csv \\\n --outdir \\\n --mode segfree\n```\n\n### Run preview mode
\n\n`BAYSOR_PREVIEW`\n\n```bash\nnextflow run nf-core/spatialaxe \\\n -profile \\\n --input samplesheet.csv \\\n --outdir \\\n --mode preview\n```\n\n### Run just the quality control
\n\n```bash\nnextflow run nf-core/spatialaxe \\\n -profile \\\n --input samplesheet.csv \\\n --outdir \\\n --mode qc\n```\n\n### Additional information\n\n> [!WARNING]\n> Please provide pipeline parameters via the CLI or Nextflow `-params-file` option. Custom config files including those provided by the `-c` Nextflow option can be used to provide any configuration _**except for parameters**_; see [docs](https://nf-co.re/docs/usage/getting_started/configuration#custom-configuration-files).\n\nFor more details and further functionality, please refer to the [usage documentation](https://nf-co.re/spatialaxe/usage) and the [parameter documentation](https://nf-co.re/spatialaxe/parameters).\n\n## Pipeline output\n\nTo see the results of an example test run with a full size dataset refer to the [results](https://nf-co.re/spatialaxe/results) tab on the nf-core website pipeline page.\nFor more details about the output files and reports, please refer to the\n[output documentation](https://nf-co.re/spatialaxe/output).\n\n## Runtime and resource estimations\n\n| Tool | Compute | Runtime (min / med / max) | Peak RSS (min / med / max) |\n| ------------------------- | ------- | ------------------------- | -------------------------- |\n| Cellpose | GPU | 1m / 4m / 1.4h | 10 GB / 26 GB / 554 GB |\n| Cellpose | CPU | 1.3h / 2.3h / 6.5h | 161 GB / 426 GB / 1115 GB |\n| StarDist | GPU | 1m / 4m / 7m | 5 GB / 12 GB / 18 GB |\n| StarDist | CPU | 5m / 6m / 7m | 18 GB / 18 GB / 18 GB |\n| Segger (create_dataset) | GPU | 2m / 9m / 31m | 1.7 GB / 14 GB / 50 GB |\n| Segger (create_dataset) | CPU | 13m / 21m / 46m | 13 GB / 19 GB / 49 GB |\n| Segger (train) | GPU | 10m / 43m / 2.9h | 30 GB / 33 GB / 60 GB |\n| Segger (predict) | GPU | 2m / 16m / 59m | 10 GB / 25 GB / 87 GB |\n| Baysor (whole-image) | CPU | 2m / 30m / 17h | 6 GB / 10 GB / 650 GB |\n| Baysor (tiled) | CPU | 1m / 18m / 13h | 0.2 GB / 34 GB / 530 GB |\n| Proseg | CPU | 1m / 18m / 6.8h | 279 MB / 3.8 GB / 136 GB |\n| XeniumRanger (resegment) | CPU | 18m / 39m / 3.7h | 28 GB / 54 GB / 60 GB |\n| XeniumRanger (import_seg) | CPU | 2m / 7m / 2.7h | 2.6 GB / 11 GB / 51 GB |\n| Ficture (preprocess) | CPU | 3m / 4m / 13m | 331 MB / 357 MB / 21 GB |\n\n- Cellpose GPU vs CPU: 35x faster on GPU (4m median vs 2.3h), 16x less memory (26 GB vs 426 GB)\n- Segger: Only tool that truly requires GPU for all 3 steps (create_dataset, train, predict)\n- StarDist: Very fast on CPU, GPU is not necessary to run its default model\n\n## Credits\n\nnf-core/spatialaxe is mainly developed by [Sameesh Kher](https://github.com/khersameesh24), [Dongze He](https://github.com/dongzehe), and [Florian Heyl](https://github.com/heylf).\n\nWe thank the following people for their extensive assistance in the development of this pipeline:\n\n- Tobias Krause\n- Kre\u0161imir Be\u0161tak (kbestak)\n- Matthias H\u00f6rtenhuber (mashehu)\n- Maxime Garcia (maxulysse)\n- K\u00fcbra Narc\u0131 (kubranarci)\n\n## Contributions and Support\n\nIf you would like to contribute to this pipeline, please see the [contributing guidelines](.github/CONTRIBUTING.md).\n\nFor further information or help, don't hesitate to get in touch on the [Slack `#spatialaxe` channel](https://nfcore.slack.com/channels/spatialaxe) (you can join with [this invite](https://nf-co.re/join/slack)).\n\n## Citations\n\nIf you use nf-core/spatialaxe for your analysis, please cite it using the following doi: [10.5281/zenodo.20733817](https://doi.org/10.5281/zenodo.20733817)\n\nAn extensive list of references for the tools used by the pipeline can be found in the [`CITATIONS.md`](CITATIONS.md) file.\n\nYou can cite the `nf-core` publication as follows:\n\n> **The nf-core framework for community-curated bioinformatics pipelines.**\n>\n> Philip Ewels, Alexander Peltzer, Sven Fillinger, Harshil Patel, Johannes Alneberg, Andreas Wilm, Maxime Ulysse Garcia, Paolo Di Tommaso & Sven Nahnsen.\n>\n> _Nat Biotechnol._ 2020 Feb 13. doi: [10.1038/s41587-020-0439-x](https://dx.doi.org/10.1038/s41587-020-0439-x).\n", "hasPart": [ { "@id": "main.nf" @@ -198,7 +198,7 @@ "url": { "@id": "https://www.nextflow.io/" }, - "version": "!>=25.04.0" + "version": "!>=26.04.6" }, { "@id": "#ae53c394-e66c-4071-90b6-f65fa2ca9cde", diff --git a/subworkflows/local/spatialdata_write_meta_merge/main.nf b/subworkflows/local/spatialdata_write_meta_merge/main.nf index 24ce9e90..4b7b0180 100644 --- a/subworkflows/local/spatialdata_write_meta_merge/main.nf +++ b/subworkflows/local/spatialdata_write_meta_merge/main.nf @@ -15,10 +15,14 @@ workflow SPATIALDATA_WRITE_META_MERGE { cell_segmentation_only // value: bool mode // value: pipeline mode (image/coordinate/...) nucleus_segmentation_only // value: bool + run_qc // value: bool main: ch_segmented_object = channel.empty() + sd_redefined_bundle = channel.empty() + sd_merged_bundle = channel.empty() + sd_metadata = channel.empty() // check segmentation - only nuclei, cells or both cells & nuclei if (mode == 'image') { @@ -38,44 +42,54 @@ workflow SPATIALDATA_WRITE_META_MERGE { } // set all boundaries as false - default - if (mode == 'coordinate') { + if (mode == 'coordinate' || mode == 'qc') { ch_segmented_object = channel.value([]) } + if (mode == 'qc' || run_qc) { + ch_coordinate_space_raw = channel.value("all") + } + // write spatialdata object from the raw xenium bundle SPATIALDATA_WRITE_RAW_BUNDLE( ch_bundle_path, 'raw_bundle', ch_segmented_object, - ch_coordinate_space, + ch_coordinate_space_raw, ) + if (mode != 'qc') { - // write spatialdata object after running IMP_SEG - SPATIALDATA_WRITE_REDEFINED_BUNDLE( - ch_redefined_bundle, - 'redefined_bundle', - ch_segmented_object, - ch_coordinate_space, - ) + // write spatialdata object after running IMP_SEG + SPATIALDATA_WRITE_REDEFINED_BUNDLE( + ch_redefined_bundle, + 'redefined_bundle', + ch_segmented_object, + ch_coordinate_space, + ) + sd_redefined_bundle = SPATIALDATA_WRITE_REDEFINED_BUNDLE.out.spatialdata - // merge raw & redefined spatialdata objects - SPATIALDATA_MERGE_RAW_REDEFINED( - SPATIALDATA_WRITE_RAW_BUNDLE.out.spatialdata.combine(ch_redefined_bundle, by: 0), - 'merged_bundle' - ) + // merge raw & redefined spatialdata objects + SPATIALDATA_MERGE_RAW_REDEFINED( + SPATIALDATA_WRITE_RAW_BUNDLE.out.spatialdata.combine(ch_redefined_bundle, by: 0), + 'merged_bundle' + ) + sd_merged_bundle = SPATIALDATA_MERGE_RAW_REDEFINED.out.merged_bundle - // write metadata with spatialdata object - SPATIALDATA_META( - SPATIALDATA_MERGE_RAW_REDEFINED.out.merged_bundle.combine(ch_bundle_path, by: 0), - 'metadata' - ) + // write metadata with spatialdata object + SPATIALDATA_META( + SPATIALDATA_MERGE_RAW_REDEFINED.out.merged_bundle.combine(ch_bundle_path, by: 0), + 'metadata' + ) + sd_metadata = SPATIALDATA_META.out.metadata + + } emit: sd_raw_bundle = SPATIALDATA_WRITE_RAW_BUNDLE.out.spatialdata // channel: [ val(meta), "spatialdata_raw" ] - sd_redefined_bundle = SPATIALDATA_WRITE_REDEFINED_BUNDLE.out.spatialdata // channel: [ val(meta), "spatialdata_redefined" ] - sd_merged_bundle = SPATIALDATA_MERGE_RAW_REDEFINED.out.merged_bundle // channel: [ val(meta), "spatialdata_merged" ] - sd_metadata = SPATIALDATA_META.out.metadata // channel: [ val(meta), "spatialdata_meta" ] + sd_redefined_bundle = sd_redefined_bundle // channel: [ val(meta), "spatialdata_redefined" ] + sd_merged_bundle = sd_merged_bundle // channel: [ val(meta), "spatialdata_merged" ] + sd_metadata = sd_metadata // channel: [ val(meta), "spatialdata_meta" ] } diff --git a/subworkflows/local/spoqc/main.nf b/subworkflows/local/spoqc/main.nf new file mode 100644 index 00000000..3d696703 --- /dev/null +++ b/subworkflows/local/spoqc/main.nf @@ -0,0 +1,596 @@ +// +// generate spatialdata object from the spatialxe layers +// + +include { SPATIALDATA_WRITE as SPATIALDATA_WRITE_RAW_BUNDLE } from '../../../modules/local/spatialdata/write/main' +// spoQC general stuff +include { SPOQC_ANNOTATION } from '../../../modules/local/spoQC/annotation/main' +include { SPOQC_WHOLE_SLIDE } from '../../../modules/local/spoQC/whole_slide/main' +include { SPOQC_GENERAL } from '../../../modules/local/spoQC/general/main' +include { SPOQC_BUBBLE } from '../../../modules/local/spoQC/bubble/main' +include { SPOQC_DOUBLET } from '../../../modules/local/spoQC/doublet/main' +include { SPOQC_VOID } from '../../../modules/local/spoQC/void/main' +include { SPOQC_CELL } from '../../../modules/local/spoQC/cell/main' +include { SPOQC_AMBIENT } from '../../../modules/local/spoQC/ambient/main' +// spoQC hqcr +include { SPOQC_HQCR_IDENT } from '../../../modules/local/spoQC/hqcr_ident/main' +include { SPOQC_HQCR_CELLTYPE } from '../../../modules/local/spoQC/hqcr_celltype/main' +// spoQC hqpr +include { SPOQC_HQPR_METRICES } from '../../../modules/local/spoQC/hqpr_metrices/main' +include { SPOQC_HQPR_CLUSTERING } from '../../../modules/local/spoQC/hqpr_clustering/main' +include { SPOQC_HQPR_REFINEMENT } from '../../../modules/local/spoQC/hqpr_refinement/main' +include { SPOQC_HQPR_BOUNDING_BOX } from '../../../modules/local/spoQC/hqpr_bounding_box/main' +include { SPOQC_HQPR_CELLTYPE } from '../../../modules/local/spoQC/hqpr_celltype/main' +// spoQC hqtr +include { SPOQC_HQTR_METRICES } from '../../../modules/local/spoQC/hqtr_metrices/main' +include { SPOQC_HQTR_AC } from '../../../modules/local/spoQC/hqtr_ac/main' +include { SPOQC_HQTR_QV } from '../../../modules/local/spoQC/hqtr_qv/main' +include { SPOQC_HQTR_CLUSTERING } from '../../../modules/local/spoQC/hqtr_clustering/main' +include { SPOQC_HQTR_REFINEMENT } from '../../../modules/local/spoQC/hqtr_refinement/main' +include { SPOQC_HQTR_BOUNDING_BOX } from '../../../modules/local/spoQC/hqtr_bounding_box/main' +include { SPOQC_HQTR_CELLTYPE } from '../../../modules/local/spoQC/hqtr_celltype/main' +// spoQC downstream +include { SPOQC_COMBINE_MASKS } from '../../../modules/local/spoQC/combine_masks/main' +include { SPOQC_TRANSCRIPT } from '../../../modules/local/spoQC/transcript/main' +include { SPOQC_CELLCYCLE } from '../../../modules/local/spoQC/cellcycle/main' +include { SPOQC_MODEL } from '../../../modules/local/spoQC/model/main' +include { SPOQC_MARKER } from '../../../modules/local/spoQC/marker/main' +// spoQC final analysis +include { SPOQC_ANALYSIS_OVERVIEW } from '../../../modules/local/spoQC/analysis_overview/main' +include { SPOQC_ANALYSIS_CATEGORY } from '../../../modules/local/spoQC/analysis_category/main' +include { SPOQC_ANALYSIS_CLUSTER } from '../../../modules/local/spoQC/analysis_cluster/main' +// spoQC final report +include { SPOQC_FINALREPORT } from '../../../modules/local/spoQC/finalreport/main' + +workflow SPOQC { + + take: + ch_sd // channel: [ val(meta), [ "path-to-spatialdata-bundle" ] ] + ch_annotation_src // channel: [ val(meta), "path-to-annotation-file" ] + ch_stainings // channel: [ val(meta), val(staining) ] - one item per sample x its own staining + + main: + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // General + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + // Keep only actual, usable annotations (non-null and not an empty list). + ch_annotation_present = ch_annotation_src.filter { _meta, a -> a && (!(a instanceof List) || !a.isEmpty()) } + + // Pair each sample's spatialdata bundle with its own annotation by meta.id. + ch_sd + .join(ch_annotation_src, by: 0) + .multiMap { meta, spatialdata, annotation -> + sd: [meta, spatialdata] + annotation: annotation + } + .set { ch_sd_annotation_src } + + SPOQC_ANNOTATION( + ch_sd_annotation_src.sd, + ch_sd_annotation_src.annotation, + "annotation", + ) + ch_annotation_path = ch_annotation_present.mix( + SPOQC_ANNOTATION.out.annotation + ) + + // Shared join of ch_sd + ch_annotation_path by meta.id, split back into + // the two positional args each downstream module expects. + ch_sd + .join(ch_annotation_path, by: 0, remainder: true) + .multiMap { meta, spatialdata, annotation -> + sd: [meta, spatialdata] + annotation: annotation ?: [] + } + .set { ch_sd_annotation } + + SPOQC_GENERAL( + ch_sd_annotation.sd, + ch_sd_annotation.annotation, + "generalqc", + ) + + SPOQC_WHOLE_SLIDE( + ch_sd, + "whole_slide_qc", + ) + + SPOQC_BUBBLE( + ch_sd, + "bubbleqc", + ) + + SPOQC_DOUBLET( + ch_sd_annotation.sd, + ch_sd_annotation.annotation, + "doubletqc", + ) + + SPOQC_VOID( + ch_sd, + "voidqc", + ) + + SPOQC_CELL( + ch_sd, + "cellqc", + ) + + SPOQC_AMBIENT( + ch_sd, + "ambientqc", + ) + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // HQCR + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + SPOQC_HQCR_IDENT( + ch_sd, + "hqcr_ident", + SPOQC_GENERAL.out.tmp.map { _meta, f -> f }, + SPOQC_BUBBLE.out.tmp.map { _meta, f -> f }, + SPOQC_DOUBLET.out.tmp.map { _meta, f -> f }, + SPOQC_VOID.out.tmp.map { _meta, f -> f }, + SPOQC_CELL.out.tmp.map { _meta, f -> f }, + ) + + SPOQC_HQCR_CELLTYPE( + ch_sd_annotation.sd, + ch_sd_annotation.annotation, + "hqcr_celltype", + SPOQC_GENERAL.out.tmp.map { _meta, f -> f }, + SPOQC_BUBBLE.out.tmp.map { _meta, f -> f }, + SPOQC_DOUBLET.out.tmp.map { _meta, f -> f }, + SPOQC_VOID.out.tmp.map { _meta, f -> f }, + SPOQC_CELL.out.tmp.map { _meta, f -> f }, + ) + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // HQPR + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + // Combine(by: 0) scopes the cross product to matching meta, so each sample only + // ever gets crossed with its own declared stainings. + ch_spatialdata_stainings = ch_sd.combine(ch_stainings, by: 0) + + SPOQC_HQPR_METRICES( + ch_spatialdata_stainings, + "hqpr_metrices", + ) + + // Join every per-staining upstream output onto ch_spatialdata_stainings by the composite + // [meta, staining], so their outputs can arrive in a different order than ch_spatialdata_stainings + // declares them in. + ch_hqpr_clustering_input = ch_spatialdata_stainings + .map { meta, spatialdata, staining -> [[meta, staining], meta, spatialdata, staining] } + .join(SPOQC_HQPR_METRICES.out.metrices.map { meta, staining, f -> [[meta, staining], f] }) + .multiMap { _key, meta, spatialdata, staining, f -> + sd: [meta, spatialdata, staining] + metrices: [staining, f] + } + + SPOQC_HQPR_CLUSTERING( + ch_hqpr_clustering_input.sd, + "hqpr_clustering", + ch_hqpr_clustering_input.metrices, + ) + + ch_hqpr_refinement_input = ch_spatialdata_stainings + .map { meta, spatialdata, staining -> [[meta, staining], meta, spatialdata, staining] } + .join(SPOQC_HQPR_CLUSTERING.out.mask.map { meta, staining, f -> [[meta, staining], f] }) + .multiMap { _key, meta, spatialdata, staining, f -> + sd: [meta, spatialdata, staining] + mask: [staining, f] + } + + SPOQC_HQPR_REFINEMENT( + ch_hqpr_refinement_input.sd, + "hqpr_refinement", + ch_hqpr_refinement_input.mask, + ) + + ch_hqpr_bounding_box_input = ch_spatialdata_stainings + .map { meta, spatialdata, staining -> [[meta, staining], meta, spatialdata, staining] } + .join(SPOQC_HQPR_REFINEMENT.out.mask_smoothed.map { meta, staining, f -> [[meta, staining], f] }) + .multiMap { _key, meta, spatialdata, staining, f -> + sd: [meta, spatialdata, staining] + mask_smoothed: [staining, f] + } + + SPOQC_HQPR_BOUNDING_BOX( + ch_hqpr_bounding_box_input.sd, + "hqpr_bounding_box", + ch_hqpr_bounding_box_input.mask_smoothed, + ) + + // Same meta.id-keyed pairing as ch_sd_annotation, extended with staining, and further + // joined (by the same composite [meta, staining] key) with the HQPR mask/mask_smoothed + // outputs so sd, annotation, and masks all come from the same joined row. + ch_sd + .join(ch_annotation_path, by: 0, remainder: true) + .map { meta, spatialdata, annotation -> [meta, spatialdata, annotation ?: []] } + .combine(ch_stainings, by: 0) + .map { meta, spatialdata, annotation, staining -> [[meta, staining], meta, spatialdata, annotation, staining] } + .join( + SPOQC_HQPR_CLUSTERING.out.mask.map { meta, staining, f -> [[meta, staining], f] } + .join(SPOQC_HQPR_REFINEMENT.out.mask_smoothed.map { meta, staining, f -> [[meta, staining], f] }) + ) + .multiMap { _key, meta, spatialdata, annotation, staining, mask, mask_smoothed -> + sd: [meta, spatialdata, staining] + annotation: [annotation, staining] + masks: [staining, mask, mask_smoothed] + } + .set { ch_sd_annotation_stainings } + + SPOQC_HQPR_CELLTYPE( + ch_sd_annotation_stainings.sd, + ch_sd_annotation_stainings.annotation, + "hqpr_celltype", + SPOQC_GENERAL.out.tmp.combine(ch_stainings, by: 0).map { _meta, f, staining -> [f, staining] }, + SPOQC_BUBBLE.out.tmp.combine(ch_stainings, by: 0).map { _meta, f, staining -> [f, staining] }, + SPOQC_DOUBLET.out.tmp.combine(ch_stainings, by: 0).map { _meta, f, staining -> [f, staining] }, + SPOQC_VOID.out.tmp.combine(ch_stainings, by: 0).map { _meta, f, staining -> [f, staining] }, + SPOQC_CELL.out.tmp.combine(ch_stainings, by: 0).map { _meta, f, staining -> [f, staining] }, + ch_sd_annotation_stainings.masks, + ) + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // HQTR + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + SPOQC_HQTR_METRICES( + ch_sd, + "hqtr_metrices", + ) + + SPOQC_HQTR_AC( + ch_sd, + "hqtr_ac", + SPOQC_AMBIENT.out.tmp.map { _meta, f -> f }, + ) + + SPOQC_HQTR_QV( + ch_sd, + "hqtr_qv", + ) + + SPOQC_HQTR_CLUSTERING( + ch_sd, + "hqtr_clustering", + SPOQC_HQTR_METRICES.out.metrices.map { _meta, f -> f }, + SPOQC_HQTR_QV.out.tmp.map { _meta, f -> f }, + SPOQC_HQTR_AC.out.tmp.map { _meta, f -> f }, + ) + + SPOQC_HQTR_REFINEMENT( + ch_sd, + "hqtr_refinement", + SPOQC_HQTR_CLUSTERING.out.mask.map { _meta, f -> f }, + ) + + SPOQC_HQTR_BOUNDING_BOX( + ch_sd, + "hqtr_bounding_box", + SPOQC_HQTR_REFINEMENT.out.mask_smoothed.map { _meta, f -> f }, + ) + + SPOQC_HQTR_CELLTYPE( + ch_sd_annotation.sd, + ch_sd_annotation.annotation, + "hqtr_celltype", + SPOQC_GENERAL.out.tmp.map { _meta, f -> f }, + SPOQC_BUBBLE.out.tmp.map { _meta, f -> f }, + SPOQC_DOUBLET.out.tmp.map { _meta, f -> f }, + SPOQC_VOID.out.tmp.map { _meta, f -> f }, + SPOQC_CELL.out.tmp.map { _meta, f -> f }, + SPOQC_HQTR_CLUSTERING.out.mask.map { _meta, f -> f }, + SPOQC_HQTR_REFINEMENT.out.mask_smoothed.map { _meta, f -> f }, + ) + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Downstream + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + // As above: join every input onto ch_spatialdata_stainings by the composite [meta, staining] + // key in one shot, so all 8 SPOQC_COMBINE_MASKS args come from the same row and can never be + // cross-wired between stainings. + ch_combine_masks_input = ch_spatialdata_stainings + .map { meta, spatialdata, staining -> [[meta, staining], meta, spatialdata, staining] } + .join(SPOQC_HQCR_IDENT.out.mask.combine(ch_stainings, by: 0).map { meta, f, staining -> [[meta, staining], f] }) + .join(SPOQC_HQPR_CLUSTERING.out.mask.map { meta, staining, f -> [[meta, staining], f] }) + .join(SPOQC_HQTR_CLUSTERING.out.mask.combine(ch_stainings, by: 0).map { meta, f, staining -> [[meta, staining], f] }) + .join(SPOQC_HQCR_IDENT.out.mask_smoothed.combine(ch_stainings, by: 0).map { meta, f, staining -> [[meta, staining], f] }) + .join(SPOQC_HQPR_REFINEMENT.out.mask_smoothed.map { meta, staining, f -> [[meta, staining], f] }) + .join(SPOQC_HQTR_REFINEMENT.out.mask_smoothed.combine(ch_stainings, by: 0).map { meta, f, staining -> [[meta, staining], f] }) + .multiMap { _key, meta, spatialdata, staining, hqcr_mask, hqpr_mask, hqtr_mask, hqcr_mask_smoothed, hqpr_mask_smoothed, hqtr_mask_smoothed -> + sd: [meta, spatialdata, staining] + hqcr_mask: [hqcr_mask, staining] + hqpr_mask: [staining, hqpr_mask] + hqtr_mask: [hqtr_mask, staining] + hqcr_mask_smoothed: [hqcr_mask_smoothed, staining] + hqpr_mask_smoothed: [staining, hqpr_mask_smoothed] + hqtr_mask_smoothed: [hqtr_mask_smoothed, staining] + } + + SPOQC_COMBINE_MASKS( + ch_combine_masks_input.sd, + "combine_masks", + ch_combine_masks_input.hqcr_mask, + ch_combine_masks_input.hqpr_mask, + ch_combine_masks_input.hqtr_mask, + ch_combine_masks_input.hqcr_mask_smoothed, + ch_combine_masks_input.hqpr_mask_smoothed, + ch_combine_masks_input.hqtr_mask_smoothed, + ) + + SPOQC_TRANSCRIPT( + ch_sd_annotation.sd, + ch_sd_annotation.annotation, + "transcriptqc", + ) + + SPOQC_CELLCYCLE( + ch_sd, + "cellcycleqc", + ) + + SPOQC_MODEL( + ch_sd, + "modelqc", + ) + + // SPOQC_MARKER( + // ch_sd, + // ch_annotation_path, + // "markerqc" + // ) + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Analysis + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + // Group each sample's per-staining HQPR files by meta, so a sample's ANALYSIS_* task only + // ever receives its own HQPR files. + ch_files_hqpr_metrics = SPOQC_HQPR_METRICES.out.metrices + .map { meta, _staining, p -> [meta, p] } + .groupTuple(by: 0) + + ch_files_hqpr_masks = SPOQC_HQPR_CLUSTERING.out.mask + .map { meta, _staining, p -> [meta, p] } + .groupTuple(by: 0) + + ch_files_hqpr_masks_smoothed = SPOQC_HQPR_REFINEMENT.out.mask_smoothed + .map { meta, _staining, p -> [meta, p] } + .groupTuple(by: 0) + + ch_sd + .join(ch_annotation_path, by: 0, remainder: true) + .join(SPOQC_GENERAL.out.tmp, by: 0) + .join(SPOQC_BUBBLE.out.tmp, by: 0) + .join(SPOQC_DOUBLET.out.tmp, by: 0) + .join(SPOQC_VOID.out.tmp, by: 0) + .join(SPOQC_CELL.out.tmp, by: 0) + .join(SPOQC_HQCR_IDENT.out.mask, by: 0) + .join(SPOQC_HQCR_IDENT.out.mask_smoothed, by: 0) + .join(SPOQC_HQTR_QV.out.tmp, by: 0) + .join(SPOQC_HQTR_AC.out.tmp, by: 0) + .join(SPOQC_HQTR_METRICES.out.metrices, by: 0) + .join(SPOQC_HQTR_REFINEMENT.out.mask_smoothed, by: 0) + .join(SPOQC_HQTR_CLUSTERING.out.mask, by: 0) + .join(ch_files_hqpr_metrics, by: 0, remainder: true) + .join(ch_files_hqpr_masks_smoothed, by: 0, remainder: true) + .join(ch_files_hqpr_masks, by: 0, remainder: true) + .multiMap { meta, spatialdata, annotation, + general, bubble, doublet, void_qc, cell, + hqcr_mask, hqcr_mask_smoothed, + hqtr_qv, hqtr_ac, hqtr_metrices, hqtr_mask_smoothed, hqtr_mask, + hqpr_metrics, hqpr_masks_smoothed, hqpr_masks -> + sd: [meta, spatialdata] + annotation: annotation ?: [] + general_tmp: general + bubble_tmp: bubble + doublet_tmp: doublet + void_tmp: void_qc + cell_tmp: cell + hqcr_mask: hqcr_mask + hqcr_mask_smoothed: hqcr_mask_smoothed + hqtr_qv: hqtr_qv + hqtr_ac: hqtr_ac + hqtr_metrices: hqtr_metrices + hqtr_mask_smoothed: hqtr_mask_smoothed + hqtr_mask: hqtr_mask + hqpr_metrics: hqpr_metrics ?: [] + hqpr_masks_smoothed: hqpr_masks_smoothed ?: [] + hqpr_masks: hqpr_masks ?: [] + } + .set { ch_analysis_inputs } + + SPOQC_ANALYSIS_OVERVIEW( + ch_analysis_inputs.sd, + ch_analysis_inputs.annotation, + "analysis_overview", + ch_analysis_inputs.general_tmp, + ch_analysis_inputs.bubble_tmp, + ch_analysis_inputs.doublet_tmp, + ch_analysis_inputs.void_tmp, + ch_analysis_inputs.cell_tmp, + ch_analysis_inputs.hqcr_mask, + ch_analysis_inputs.hqcr_mask_smoothed, + ch_analysis_inputs.hqtr_qv, + ch_analysis_inputs.hqtr_ac, + ch_analysis_inputs.hqtr_metrices, + ch_analysis_inputs.hqtr_mask_smoothed, + ch_analysis_inputs.hqtr_mask, + ch_analysis_inputs.hqpr_metrics, + ch_analysis_inputs.hqpr_masks_smoothed, + ch_analysis_inputs.hqpr_masks, + ) + + SPOQC_ANALYSIS_CATEGORY( + ch_analysis_inputs.sd, + ch_analysis_inputs.annotation, + "analysis_category", + ch_analysis_inputs.general_tmp, + ch_analysis_inputs.bubble_tmp, + ch_analysis_inputs.doublet_tmp, + ch_analysis_inputs.void_tmp, + ch_analysis_inputs.cell_tmp, + ch_analysis_inputs.hqcr_mask, + ch_analysis_inputs.hqcr_mask_smoothed, + ch_analysis_inputs.hqtr_qv, + ch_analysis_inputs.hqtr_ac, + ch_analysis_inputs.hqtr_metrices, + ch_analysis_inputs.hqtr_mask_smoothed, + ch_analysis_inputs.hqtr_mask, + ch_analysis_inputs.hqpr_metrics, + ch_analysis_inputs.hqpr_masks_smoothed, + ch_analysis_inputs.hqpr_masks, + ) + + SPOQC_ANALYSIS_CLUSTER( + ch_analysis_inputs.sd, + ch_analysis_inputs.annotation, + "analysis_cluster", + ch_analysis_inputs.general_tmp, + ch_analysis_inputs.bubble_tmp, + ch_analysis_inputs.doublet_tmp, + ch_analysis_inputs.void_tmp, + ch_analysis_inputs.cell_tmp, + ch_analysis_inputs.hqcr_mask, + ch_analysis_inputs.hqcr_mask_smoothed, + ch_analysis_inputs.hqtr_qv, + ch_analysis_inputs.hqtr_ac, + ch_analysis_inputs.hqtr_metrices, + ch_analysis_inputs.hqtr_mask_smoothed, + ch_analysis_inputs.hqtr_mask, + ch_analysis_inputs.hqpr_metrics, + ch_analysis_inputs.hqpr_masks_smoothed, + ch_analysis_inputs.hqpr_masks, + ) + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Final Report + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + // Group the per-staining report fragments by meta, so a + // sample's final report only ever bundles its own report fragments. + ch_report_hqpr_metrices = SPOQC_HQPR_METRICES.out.report + .map { meta, _staining, p -> [meta, p] } + .groupTuple(by: 0) + ch_report_hqpr_clustering = SPOQC_HQPR_CLUSTERING.out.report + .map { meta, _staining, p -> [meta, p] } + .groupTuple(by: 0) + ch_report_hqpr_refinement = SPOQC_HQPR_REFINEMENT.out.report + .map { meta, _staining, p -> [meta, p] } + .groupTuple(by: 0) + ch_report_hqpr_bounding_box = SPOQC_HQPR_BOUNDING_BOX.out.report + .map { meta, _staining, p -> [meta, p] } + .groupTuple(by: 0) + ch_report_combine_masks = SPOQC_COMBINE_MASKS.out.report + .groupTuple(by: 0) + + ch_sd + .join(SPOQC_GENERAL.out.report, by: 0) + .join(SPOQC_DOUBLET.out.report, by: 0) + .join(SPOQC_VOID.out.report, by: 0) + .join(SPOQC_CELL.out.report, by: 0) + .join(SPOQC_HQCR_IDENT.out.report, by: 0) + .join(SPOQC_HQCR_CELLTYPE.out.report, by: 0) + .join(ch_report_hqpr_metrices, by: 0, remainder: true) + .join(ch_report_hqpr_clustering, by: 0, remainder: true) + .join(ch_report_hqpr_refinement, by: 0, remainder: true) + .join(ch_report_hqpr_bounding_box, by: 0, remainder: true) + .join(SPOQC_HQPR_CELLTYPE.out.report, by: 0) + .join(SPOQC_HQTR_METRICES.out.report, by: 0) + .join(SPOQC_HQTR_AC.out.report, by: 0) + .join(SPOQC_HQTR_QV.out.report, by: 0) + .join(SPOQC_HQTR_CLUSTERING.out.report, by: 0) + .join(SPOQC_HQTR_REFINEMENT.out.report, by: 0) + .join(SPOQC_HQTR_BOUNDING_BOX.out.report, by: 0) + .join(SPOQC_HQTR_CELLTYPE.out.report, by: 0) + .join(ch_report_combine_masks, by: 0, remainder: true) + .join(SPOQC_TRANSCRIPT.out.report, by: 0) + .join(SPOQC_CELLCYCLE.out.report, by: 0) + .join(SPOQC_MODEL.out.report, by: 0) + .join(SPOQC_ANALYSIS_OVERVIEW.out.report, by: 0) + .join(SPOQC_ANALYSIS_CATEGORY.out.report, by: 0) + .join(SPOQC_ANALYSIS_CLUSTER.out.report, by: 0) + .multiMap { + meta, spatialdata, + report_general, report_doublet, report_void, report_cell, + report_hqcr_ident, report_hqcr_celltype, + report_hqpr_metrices, report_hqpr_clustering, report_hqpr_refinement, report_hqpr_bounding_box, + report_hqpr_celltype, + report_hqtr_metrices, report_hqtr_ac, report_hqtr_qv, report_hqtr_clustering, + report_hqtr_refinement, report_hqtr_bounding_box, report_hqtr_celltype, + report_combine_masks, report_transcript, report_cellcycle, report_model, + report_analysis_overview, report_analysis_category, report_analysis_cluster -> + sd: [meta, spatialdata] + report_general: report_general + report_doublet: report_doublet + report_void: report_void + report_cell: report_cell + report_hqcr_ident: report_hqcr_ident + report_hqcr_celltype: report_hqcr_celltype + report_hqpr_metrices: report_hqpr_metrices ?: [] + report_hqpr_clustering: report_hqpr_clustering ?: [] + report_hqpr_refinement: report_hqpr_refinement ?: [] + report_hqpr_bounding_box: report_hqpr_bounding_box ?: [] + report_hqpr_celltype: report_hqpr_celltype + report_hqtr_metrices: report_hqtr_metrices + report_hqtr_ac: report_hqtr_ac + report_hqtr_qv: report_hqtr_qv + report_hqtr_clustering: report_hqtr_clustering + report_hqtr_refinement: report_hqtr_refinement + report_hqtr_bounding_box: report_hqtr_bounding_box + report_hqtr_celltype: report_hqtr_celltype + report_combine_masks: report_combine_masks ?: [] + report_transcript: report_transcript + report_cellcycle: report_cellcycle + report_model: report_model + report_analysis_overview: report_analysis_overview + report_analysis_category: report_analysis_category + report_analysis_cluster: report_analysis_cluster + } + .set { ch_finalreport_inputs } + + SPOQC_FINALREPORT( + ch_finalreport_inputs.sd, + "final_report", + ch_finalreport_inputs.report_general, + ch_finalreport_inputs.report_doublet, + ch_finalreport_inputs.report_void, + ch_finalreport_inputs.report_cell, + ch_finalreport_inputs.report_hqcr_ident, + ch_finalreport_inputs.report_hqcr_celltype, + ch_finalreport_inputs.report_hqpr_metrices, + ch_finalreport_inputs.report_hqpr_clustering, + ch_finalreport_inputs.report_hqpr_refinement, + ch_finalreport_inputs.report_hqpr_bounding_box, + ch_finalreport_inputs.report_hqpr_celltype, + ch_finalreport_inputs.report_hqtr_metrices, + ch_finalreport_inputs.report_hqtr_ac, + ch_finalreport_inputs.report_hqtr_qv, + ch_finalreport_inputs.report_hqtr_clustering, + ch_finalreport_inputs.report_hqtr_refinement, + ch_finalreport_inputs.report_hqtr_bounding_box, + ch_finalreport_inputs.report_hqtr_celltype, + ch_finalreport_inputs.report_combine_masks, + ch_finalreport_inputs.report_transcript, + ch_finalreport_inputs.report_cellcycle, + ch_finalreport_inputs.report_model, + ch_finalreport_inputs.report_analysis_overview, + ch_finalreport_inputs.report_analysis_category, + ch_finalreport_inputs.report_analysis_cluster, + ) + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Output + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + emit: + + report = SPOQC_FINALREPORT.out.report +} diff --git a/subworkflows/local/spoqc/meta.yml b/subworkflows/local/spoqc/meta.yml new file mode 100644 index 00000000..215f582d --- /dev/null +++ b/subworkflows/local/spoqc/meta.yml @@ -0,0 +1,72 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/subworkflows/yaml-schema.json +name: "spoqc" +description: | + Builds a raw SpatialData object from a 10x Xenium bundle and runs the full spoQC quality-control + toolkit against it: general/bubble/doublet/void/cell/ambient QC, high-quality cell region (HQCR), + high-quality pixel region (HQPR) and high-quality transcript region (HQTR) mask + identification/clustering/refinement, mask combination, transcript/cell-cycle/model QC, and the + downstream overview/category/cluster analysis reports. +keywords: + - spatialdata + - xenium + - quality control + - spoqc + - hqcr + - hqpr + - hqtr +components: + - spatialdata/write + - spoQC/annotation + - spoQC/general + - spoQC/whole_slide + - spoQC/bubble + - spoQC/doublet + - spoQC/void + - spoQC/cell + - spoQC/ambient + - spoQC/hqcr_ident + - spoQC/hqcr_celltype + - spoQC/hqpr_metrices + - spoQC/hqpr_clustering + - spoQC/hqpr_refinement + - spoQC/hqpr_bounding_box + - spoQC/hqpr_celltype + - spoQC/hqtr_metrices + - spoQC/hqtr_ac + - spoQC/hqtr_qv + - spoQC/hqtr_clustering + - spoQC/hqtr_refinement + - spoQC/hqtr_bounding_box + - spoQC/hqtr_celltype + - spoQC/combine_masks + - spoQC/transcript + - spoQC/cellcycle + - spoQC/model + - spoQC/analysis_overview + - spoQC/analysis_category + - spoQC/analysis_cluster + - spoQC/finalreport + # Not currently wired into this subworkflow (calls are commented out in main.nf): + # spoQC/marker +input: + - ch_bundle_path: + description: | + Directory containing the raw Xenium bundle + Structure: [ val(meta), path("path-to-xenium-bundle") ] + - ch_annotation_src: + description: | + Optional pre-existing cell-type annotation file. When empty/absent, SPOQC_ANNOTATION + generates one instead and it is mixed back into the downstream annotation channel. + Structure: [ path("path-to-annotation-file") ] or [ [] ] + - ch_stainings: + description: | + List of staining indices to iterate the HQPR steps over + Structure: [ [ 1, 2, ... ] ] +output: + - report: + description: | + Final combined spoQC HTML report +authors: + - "@heylf" +maintainers: + - "@heylf" diff --git a/subworkflows/local/utils_nfcore_spatialaxe_pipeline/main.nf b/subworkflows/local/utils_nfcore_spatialaxe_pipeline/main.nf index 6f230ef0..f4786715 100644 --- a/subworkflows/local/utils_nfcore_spatialaxe_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_spatialaxe_pipeline/main.nf @@ -98,7 +98,8 @@ workflow PIPELINE_INITIALISATION { show_hidden, before_text, after_text, - command + command, + null ) // @@ -135,8 +136,8 @@ workflow PIPELINE_INITIALISATION { try { channel.fromList(samplesheetToList(input, "${projectDir}/assets/schema_input.json")) - .map { meta, bundle, image -> - return [[id: meta.id], bundle, image] + .map { meta, bundle, image, annotation, stainings -> + return [[id: meta.id], bundle, image, annotation, stainings] } .set { ch_samplesheet } @@ -231,7 +232,7 @@ def validateInputParameters( } // check if the samplesheet provided with the test config is assets/samplesheet.csv - if (workflow.profile.contains('test') && !"${input}".endsWith("assets/samplesheet.csv")) { + if (workflow.profile.contains('test') && !"${input}".endsWith("assets/samplesheet.csv") && !workflow.profile.contains('test_full')) { error("❌ Error: Use the samplesheet at: ${projectDir}/assets/samplesheet.csv with `--input` when running the pipeline in test profile.") } diff --git a/subworkflows/nf-core/utils_nextflow_pipeline/main.nf b/subworkflows/nf-core/utils_nextflow_pipeline/main.nf index d6e593e8..37939acd 100644 --- a/subworkflows/nf-core/utils_nextflow_pipeline/main.nf +++ b/subworkflows/nf-core/utils_nextflow_pipeline/main.nf @@ -73,11 +73,23 @@ def getWorkflowVersion() { def dumpParametersToJSON(outdir) { def timestamp = new java.util.Date().format('yyyy-MM-dd_HH-mm-ss') def filename = "params_${timestamp}.json" - def temp_pf = new File(workflow.launchDir.toString(), ".${filename}") - def jsonStr = groovy.json.JsonOutput.toJson(params) + def temp_pf = workflow.launchDir.resolve(".${filename}") + def jsonGenerator = new groovy.json.JsonGenerator.Options() + .excludeNulls() + .addConverter(Path) { Path path -> path.toUriString() } + .addConverter(Duration) { Duration duration -> duration.toMillis() } + .addConverter(MemoryUnit) { MemoryUnit memory -> memory.toBytes() } + .addConverter(nextflow.script.types.VersionNumber) { nextflow.script.types.VersionNumber version -> version.toString() } + .build() + def jsonStr = jsonGenerator.toJson(params) temp_pf.text = groovy.json.JsonOutput.prettyPrint(jsonStr) - - nextflow.extension.FilesEx.copyTo(temp_pf.toPath(), "${outdir}/pipeline_info/params_${timestamp}.json") + if (outdir instanceof Path) { + temp_pf.copyTo(outdir.resolve("pipeline_info/${filename}")) + } else if (outdir instanceof String) { + temp_pf.copyTo("${outdir}/pipeline_info/params_${timestamp}.json") + } else { + log.warn("Could not determine type of outdir, parameters JSON file will not be copied to output directory!") + } temp_pf.delete() } diff --git a/subworkflows/nf-core/utils_nextflow_pipeline/tests/main.function.nf.test b/subworkflows/nf-core/utils_nextflow_pipeline/tests/main.function.nf.test index 897d6681..68718e4f 100644 --- a/subworkflows/nf-core/utils_nextflow_pipeline/tests/main.function.nf.test +++ b/subworkflows/nf-core/utils_nextflow_pipeline/tests/main.function.nf.test @@ -40,9 +40,9 @@ nextflow_function { } } - test("Test Function checkCondachannels") { + test("Test Function checkCondaChannels") { - function "checkCondachannels" + function "checkCondaChannels" then { assertAll( diff --git a/subworkflows/nf-core/utils_nextflow_pipeline/tests/main.function.nf.test.snap b/subworkflows/nf-core/utils_nextflow_pipeline/tests/main.function.nf.test.snap index f03a352a..e3f0baf4 100644 --- a/subworkflows/nf-core/utils_nextflow_pipeline/tests/main.function.nf.test.snap +++ b/subworkflows/nf-core/utils_nextflow_pipeline/tests/main.function.nf.test.snap @@ -9,7 +9,7 @@ }, "timestamp": "2024-02-28T12:02:05.308243" }, - "Test Function checkCondachannels": { + "Test Function checkCondaChannels": { "content": null, "meta": { "nf-test": "0.8.4", diff --git a/subworkflows/nf-core/utils_nfschema_plugin/main.nf b/subworkflows/nf-core/utils_nfschema_plugin/main.nf index ee4738c8..9ff0681f 100644 --- a/subworkflows/nf-core/utils_nfschema_plugin/main.nf +++ b/subworkflows/nf-core/utils_nfschema_plugin/main.nf @@ -22,6 +22,7 @@ workflow UTILS_NFSCHEMA_PLUGIN { before_text // string: text to show before the help message and parameters summary after_text // string: text to show after the help message and parameters summary command // string: an example command of the pipeline + cli_typecast // boolean: whether to perform typecasting of CLI parameters. Set this to `null` to use the default behaviour main: @@ -34,11 +35,11 @@ workflow UTILS_NFSCHEMA_PLUGIN { fullHelp: help_full, ] if(parameters_schema) { - help_options << [parametersSchema: parameters_schema] + help_options << [parameters_schema: parameters_schema] } log.info paramsHelp( help_options, - params.help instanceof String ? params.help : "", + (help instanceof String && help != "true") ? help : "", ) exit 0 } @@ -50,7 +51,7 @@ workflow UTILS_NFSCHEMA_PLUGIN { summary_options = [:] if(parameters_schema) { - summary_options << [parametersSchema: parameters_schema] + summary_options << [parameters_schema: parameters_schema] } log.info before_text log.info paramsSummaryLog(summary_options, input_workflow) @@ -63,7 +64,10 @@ workflow UTILS_NFSCHEMA_PLUGIN { if(validate_params) { validateOptions = [:] if(parameters_schema) { - validateOptions << [parametersSchema: parameters_schema] + validateOptions << [parameters_schema: parameters_schema] + } + if(cli_typecast != null) { + validateOptions << [cast_cli_params: cli_typecast] } validateParameters(validateOptions) } @@ -71,4 +75,3 @@ workflow UTILS_NFSCHEMA_PLUGIN { emit: dummy_emit = true } - diff --git a/subworkflows/nf-core/utils_nfschema_plugin/meta.yml b/subworkflows/nf-core/utils_nfschema_plugin/meta.yml index f7d9f028..1d8c75a9 100644 --- a/subworkflows/nf-core/utils_nfschema_plugin/meta.yml +++ b/subworkflows/nf-core/utils_nfschema_plugin/meta.yml @@ -25,6 +25,30 @@ input: option. When this input is empty it will automatically use the configured schema or "${projectDir}/nextflow_schema.json" as default. The schema should not be given in this way for meta pipelines. + - help: + type: boolean, string + description: | + Show the help message and exit. When a parameter name is given, show the help message for that parameter instead of the general help message. + - help_full: + type: boolean + description: Show the full help message and exit. + - show_hidden: + type: boolean + description: Show hidden parameters in the help message. + - before_text: + type: string + description: Text to show before the parameters summary and help message. + - after_text: + type: string + description: Text to show after the parameters summary and help message. + - command: + type: string + description: An example command to run the pipeline, to show in the help message and the summary. + - cli_typecast: + type: boolean + description: | + Whether to apply typecasting to the parameters given via the CLI before validation. + Set this to `null` to use the default behavior. output: - dummy_emit: type: boolean diff --git a/subworkflows/nf-core/utils_nfschema_plugin/tests/main.nf.test b/subworkflows/nf-core/utils_nfschema_plugin/tests/main.nf.test index c977917a..1fd1eac1 100644 --- a/subworkflows/nf-core/utils_nfschema_plugin/tests/main.nf.test +++ b/subworkflows/nf-core/utils_nfschema_plugin/tests/main.nf.test @@ -31,6 +31,7 @@ nextflow_workflow { input[6] = "" input[7] = "" input[8] = "" + input[9] = null """ } } @@ -63,6 +64,7 @@ nextflow_workflow { input[6] = "" input[7] = "" input[8] = "" + input[9] = null """ } } @@ -95,6 +97,7 @@ nextflow_workflow { input[6] = "" input[7] = "" input[8] = "" + input[9] = null """ } } @@ -127,6 +130,7 @@ nextflow_workflow { input[6] = "" input[7] = "" input[8] = "" + input[9] = null """ } } @@ -160,6 +164,7 @@ nextflow_workflow { input[6] = "Before" input[7] = "After" input[8] = "nextflow run test/test" + input[9] = null """ } } diff --git a/subworkflows/nf-core/utils_nfschema_plugin/tests/nextflow.config b/subworkflows/nf-core/utils_nfschema_plugin/tests/nextflow.config index 8d8c7371..fd71cb8f 100644 --- a/subworkflows/nf-core/utils_nfschema_plugin/tests/nextflow.config +++ b/subworkflows/nf-core/utils_nfschema_plugin/tests/nextflow.config @@ -1,5 +1,5 @@ plugins { - id "nf-schema@2.5.1" + id "nf-schema@2.7.2" } validation { diff --git a/tests/.nftignore b/tests/.nftignore index 0d1a17b8..9469e035 100644 --- a/tests/.nftignore +++ b/tests/.nftignore @@ -28,6 +28,10 @@ coordinate/spatialdata/meta/spatialdata/test_run/metadata/tables/** # xeniumranger's internal secondary analysis (clustering/PCA/UMAP/diffexp) and # resegmentation are not run-to-run reproducible; only existence is checked xeniumranger/test_run/** + +# spoQC generates thousands of non-deterministic plot/report files; only +# folder existence is checked in spoqc.nf.test, not individual file content +spoqc/** coordinate/untar/test_run/** coordinate/spatialdata/write/spatialdata/test_run/redefined_bundle/points/** coordinate/spatialdata/write/spatialdata/test_run/redefined_bundle/shapes/** @@ -50,3 +54,20 @@ coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/zarr.json coordinate/spatialdata/write/spatialdata/test_run/redefined_bundle/zarr.json coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/zarr.json coordinate/spatialdata/meta/spatialdata/test_run/metadata/zarr.json + +# spoQC (qc mode) MultiQC report embeds non-deterministic run data +qc/multiqc/multiqc_data/llms-full.txt +qc/multiqc/multiqc_data/multiqc.log +qc/multiqc/multiqc_data/multiqc.parquet +qc/multiqc/multiqc_data/multiqc_data.json +qc/multiqc/multiqc_data/multiqc_sources.txt +qc/multiqc/multiqc_plots/{svg,pdf,png}/*.{svg,pdf,png} +qc/multiqc/multiqc_report.html + +# same spatialdata/write module as coordinate mode's raw_bundle: points/shapes/tables +# parquet writers and the bundle-root zarr.json member ordering are not +# run-to-run reproducible +qc/spatialdata/write/spatialdata/test_run/spatialdata_raw/points/** +qc/spatialdata/write/spatialdata/test_run/spatialdata_raw/shapes/** +qc/spatialdata/write/spatialdata/test_run/spatialdata_raw/tables/** +qc/spatialdata/write/spatialdata/test_run/spatialdata_raw/zarr.json diff --git a/tests/coordinate_mode.nf.test.snap b/tests/coordinate_mode.nf.test.snap index 22434439..628f4439 100644 --- a/tests/coordinate_mode.nf.test.snap +++ b/tests/coordinate_mode.nf.test.snap @@ -24,7 +24,7 @@ "untar": 1.34 }, "Workflow": { - "nf-core/spatialaxe": "v1.0.1" + "nf-core/spatialaxe": "v1.0.1dev" }, "XENIUMRANGER_IMPORTSEGMENTATION": { "xeniumranger": "4.0.1.1" @@ -109,6 +109,64 @@ "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/images/morphology_focus/4/zarr.json", "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/images/morphology_focus/zarr.json", "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/images/zarr.json", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/cell_labels", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/cell_labels/0", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/cell_labels/0/c", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/cell_labels/0/c/0", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/cell_labels/0/c/0/0", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/cell_labels/0/c/0/1", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/cell_labels/0/zarr.json", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/cell_labels/1", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/cell_labels/1/c", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/cell_labels/1/c/0", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/cell_labels/1/c/0/0", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/cell_labels/1/zarr.json", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/cell_labels/2", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/cell_labels/2/c", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/cell_labels/2/c/0", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/cell_labels/2/c/0/0", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/cell_labels/2/zarr.json", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/cell_labels/3", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/cell_labels/3/c", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/cell_labels/3/c/0", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/cell_labels/3/c/0/0", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/cell_labels/3/zarr.json", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/cell_labels/4", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/cell_labels/4/c", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/cell_labels/4/c/0", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/cell_labels/4/c/0/0", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/cell_labels/4/zarr.json", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/cell_labels/zarr.json", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/nucleus_labels", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/nucleus_labels/0", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/nucleus_labels/0/c", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/nucleus_labels/0/c/0", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/nucleus_labels/0/c/0/0", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/nucleus_labels/0/c/0/1", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/nucleus_labels/0/zarr.json", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/nucleus_labels/1", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/nucleus_labels/1/c", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/nucleus_labels/1/c/0", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/nucleus_labels/1/c/0/0", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/nucleus_labels/1/zarr.json", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/nucleus_labels/2", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/nucleus_labels/2/c", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/nucleus_labels/2/c/0", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/nucleus_labels/2/c/0/0", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/nucleus_labels/2/zarr.json", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/nucleus_labels/3", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/nucleus_labels/3/c", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/nucleus_labels/3/c/0", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/nucleus_labels/3/c/0/0", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/nucleus_labels/3/zarr.json", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/nucleus_labels/4", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/nucleus_labels/4/c", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/nucleus_labels/4/c/0", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/nucleus_labels/4/c/0/0", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/nucleus_labels/4/zarr.json", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/nucleus_labels/zarr.json", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/labels/zarr.json", "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/points", "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/points/raw_transcripts", "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/points/raw_transcripts/points.parquet", @@ -119,6 +177,12 @@ "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/shapes/raw_cell_boundaries", "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/shapes/raw_cell_boundaries/shapes.parquet", "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/shapes/raw_cell_boundaries/zarr.json", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/shapes/raw_cell_circles", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/shapes/raw_cell_circles/shapes.parquet", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/shapes/raw_cell_circles/zarr.json", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/shapes/raw_nucleus_boundaries", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/shapes/raw_nucleus_boundaries/shapes.parquet", + "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/shapes/raw_nucleus_boundaries/zarr.json", "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/shapes/zarr.json", "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/tables", "coordinate/spatialdata/merge/spatialdata/test_run/merged_bundle/zarr.json", @@ -161,6 +225,64 @@ "coordinate/spatialdata/meta/spatialdata/test_run/metadata/images/morphology_focus/4/zarr.json", "coordinate/spatialdata/meta/spatialdata/test_run/metadata/images/morphology_focus/zarr.json", "coordinate/spatialdata/meta/spatialdata/test_run/metadata/images/zarr.json", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/cell_labels", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/cell_labels/0", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/cell_labels/0/c", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/cell_labels/0/c/0", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/cell_labels/0/c/0/0", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/cell_labels/0/c/0/1", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/cell_labels/0/zarr.json", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/cell_labels/1", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/cell_labels/1/c", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/cell_labels/1/c/0", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/cell_labels/1/c/0/0", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/cell_labels/1/zarr.json", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/cell_labels/2", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/cell_labels/2/c", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/cell_labels/2/c/0", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/cell_labels/2/c/0/0", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/cell_labels/2/zarr.json", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/cell_labels/3", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/cell_labels/3/c", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/cell_labels/3/c/0", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/cell_labels/3/c/0/0", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/cell_labels/3/zarr.json", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/cell_labels/4", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/cell_labels/4/c", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/cell_labels/4/c/0", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/cell_labels/4/c/0/0", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/cell_labels/4/zarr.json", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/cell_labels/zarr.json", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/nucleus_labels", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/nucleus_labels/0", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/nucleus_labels/0/c", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/nucleus_labels/0/c/0", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/nucleus_labels/0/c/0/0", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/nucleus_labels/0/c/0/1", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/nucleus_labels/0/zarr.json", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/nucleus_labels/1", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/nucleus_labels/1/c", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/nucleus_labels/1/c/0", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/nucleus_labels/1/c/0/0", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/nucleus_labels/1/zarr.json", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/nucleus_labels/2", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/nucleus_labels/2/c", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/nucleus_labels/2/c/0", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/nucleus_labels/2/c/0/0", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/nucleus_labels/2/zarr.json", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/nucleus_labels/3", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/nucleus_labels/3/c", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/nucleus_labels/3/c/0", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/nucleus_labels/3/c/0/0", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/nucleus_labels/3/zarr.json", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/nucleus_labels/4", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/nucleus_labels/4/c", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/nucleus_labels/4/c/0", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/nucleus_labels/4/c/0/0", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/nucleus_labels/4/zarr.json", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/nucleus_labels/zarr.json", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/labels/zarr.json", "coordinate/spatialdata/meta/spatialdata/test_run/metadata/points", "coordinate/spatialdata/meta/spatialdata/test_run/metadata/points/raw_transcripts", "coordinate/spatialdata/meta/spatialdata/test_run/metadata/points/raw_transcripts/points.parquet", @@ -171,6 +293,12 @@ "coordinate/spatialdata/meta/spatialdata/test_run/metadata/shapes/raw_cell_boundaries", "coordinate/spatialdata/meta/spatialdata/test_run/metadata/shapes/raw_cell_boundaries/shapes.parquet", "coordinate/spatialdata/meta/spatialdata/test_run/metadata/shapes/raw_cell_boundaries/zarr.json", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/shapes/raw_cell_circles", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/shapes/raw_cell_circles/shapes.parquet", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/shapes/raw_cell_circles/zarr.json", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/shapes/raw_nucleus_boundaries", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/shapes/raw_nucleus_boundaries/shapes.parquet", + "coordinate/spatialdata/meta/spatialdata/test_run/metadata/shapes/raw_nucleus_boundaries/zarr.json", "coordinate/spatialdata/meta/spatialdata/test_run/metadata/shapes/zarr.json", "coordinate/spatialdata/meta/spatialdata/test_run/metadata/tables", "coordinate/spatialdata/meta/spatialdata/test_run/metadata/zarr.json", @@ -213,6 +341,64 @@ "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/images/morphology_focus/4/zarr.json", "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/images/morphology_focus/zarr.json", "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/images/zarr.json", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/0", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/0/c", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/0/c/0", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/0/c/0/0", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/0/c/0/1", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/0/zarr.json", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/1", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/1/c", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/1/c/0", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/1/c/0/0", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/1/zarr.json", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/2", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/2/c", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/2/c/0", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/2/c/0/0", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/2/zarr.json", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/3", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/3/c", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/3/c/0", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/3/c/0/0", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/3/zarr.json", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/4", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/4/c", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/4/c/0", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/4/c/0/0", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/4/zarr.json", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/zarr.json", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/0", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/0/c", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/0/c/0", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/0/c/0/0", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/0/c/0/1", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/0/zarr.json", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/1", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/1/c", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/1/c/0", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/1/c/0/0", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/1/zarr.json", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/2", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/2/c", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/2/c/0", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/2/c/0/0", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/2/zarr.json", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/3", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/3/c", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/3/c/0", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/3/c/0/0", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/3/zarr.json", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/4", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/4/c", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/4/c/0", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/4/c/0/0", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/4/zarr.json", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/zarr.json", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/labels/zarr.json", "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/points", "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/points/transcripts", "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/points/transcripts/points.parquet", @@ -223,6 +409,12 @@ "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/shapes/cell_boundaries", "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/shapes/cell_boundaries/shapes.parquet", "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/shapes/cell_boundaries/zarr.json", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/shapes/cell_circles", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/shapes/cell_circles/shapes.parquet", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/shapes/cell_circles/zarr.json", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/shapes/nucleus_boundaries", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/shapes/nucleus_boundaries/shapes.parquet", + "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/shapes/nucleus_boundaries/zarr.json", "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/shapes/zarr.json", "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/tables", "coordinate/spatialdata/write/spatialdata/test_run/raw_bundle/zarr.json", @@ -285,14 +477,14 @@ [ "multiqc_citations.txt:md5,4c806e63a283ec1b7e78cdae3a923d4f", "multiqc_general_stats.txt:md5,e38eaaa52226a0d985578208b3261585", - "multiqc_software_versions.txt:md5,4c66c24e7c7ec9133e99bf68a3ab05f9", + "multiqc_software_versions.txt:md5,6b25b317ed5e9bda0d2c9ff6fe85eafb", "multiqc_xenium.txt:md5,f52e6ac3376565f7975e4e9fc23ed52a", "xenium_fov_quality_ranges.txt:md5,153e0a905781d0b1b2f01b5ea6e384a8", "xenium_segmentation.txt:md5,abd99ee57c53568a7f915bd10d2ee5af", "xenium_transcript_quality_per_sample_table.txt:md5,d0135a0006f4ad3631b9f3906cc7ee0b", "xenium_transcripts_per_gene.txt:md5,b46b3c414bc660e03e3df0345dd5e3e2", "multiqc_citations.txt:md5,4c806e63a283ec1b7e78cdae3a923d4f", - "multiqc_software_versions.txt:md5,4c66c24e7c7ec9133e99bf68a3ab05f9", + "multiqc_software_versions.txt:md5,6b25b317ed5e9bda0d2c9ff6fe85eafb", "0:md5,397dc21e479c58fd2ffcbe6c3e627683", "1:md5,f1a6b9238c3095bad7867b22f293e5e6", "zarr.json:md5,893b4d8873fc997d364288c63ef0871f", @@ -305,6 +497,31 @@ "0:md5,4890e52e8f0efc8f560bc51199d5071c", "zarr.json:md5,1f4482a1e3b6e9ab30623b0ad12f5213", "zarr.json:md5,b98263d3964a5236145ccf6c808f8919", + "0:md5,9fbd82d28a84dddd52df1b055c00d9b0", + "1:md5,3519dd1d29e589a2f0e87c2e0a20586b", + "zarr.json:md5,5ad6b55efbf7ea079dc73f60af2a12cb", + "0:md5,500e366b93160eaf9cf5ee5dcd8fdd18", + "zarr.json:md5,cf8da18bcd5b9661012f3b120d64d5d9", + "0:md5,264bcf6f0679d8cab53930b5182aa2ac", + "zarr.json:md5,a03571ade1e696e2e855ed06331acd09", + "0:md5,c416a29978b65ccec08ad8e0d3e1dcf8", + "zarr.json:md5,be459dc60bef9886df3f9e6895430f6b", + "0:md5,072da7c64c1dbe21a4d71c560431b96e", + "zarr.json:md5,73b113cb0e0dd3c688b7221003ce7062", + "zarr.json:md5,cb8cd70a8b5142c2bdcc65261f52013e", + "0:md5,adbe51c450a9a6c9086df9ebe220eefb", + "1:md5,7bb4eecf9fe3996a9b118082152a8750", + "zarr.json:md5,5ad6b55efbf7ea079dc73f60af2a12cb", + "0:md5,498463ef4033f523f8174237454f5dc7", + "zarr.json:md5,cf8da18bcd5b9661012f3b120d64d5d9", + "0:md5,a73b0d9b8919bb729c6eee8cf6632cd8", + "zarr.json:md5,a03571ade1e696e2e855ed06331acd09", + "0:md5,9efe1a6b6f8d3548f3ebdeab07e2e8b3", + "zarr.json:md5,be459dc60bef9886df3f9e6895430f6b", + "0:md5,4cd5ff674991828d1536bf85d47bcb0a", + "zarr.json:md5,73b113cb0e0dd3c688b7221003ce7062", + "zarr.json:md5,83ce0ad0ef9fc5f27b0c737f7bc2c88f", + "zarr.json:md5,0fc99ee9a39262fbd42f104d79a9edd8", "0:md5,397dc21e479c58fd2ffcbe6c3e627683", "1:md5,f1a6b9238c3095bad7867b22f293e5e6", "zarr.json:md5,893b4d8873fc997d364288c63ef0871f", @@ -317,6 +534,31 @@ "0:md5,4890e52e8f0efc8f560bc51199d5071c", "zarr.json:md5,1f4482a1e3b6e9ab30623b0ad12f5213", "zarr.json:md5,b98263d3964a5236145ccf6c808f8919", + "0:md5,9fbd82d28a84dddd52df1b055c00d9b0", + "1:md5,3519dd1d29e589a2f0e87c2e0a20586b", + "zarr.json:md5,5ad6b55efbf7ea079dc73f60af2a12cb", + "0:md5,500e366b93160eaf9cf5ee5dcd8fdd18", + "zarr.json:md5,cf8da18bcd5b9661012f3b120d64d5d9", + "0:md5,264bcf6f0679d8cab53930b5182aa2ac", + "zarr.json:md5,a03571ade1e696e2e855ed06331acd09", + "0:md5,c416a29978b65ccec08ad8e0d3e1dcf8", + "zarr.json:md5,be459dc60bef9886df3f9e6895430f6b", + "0:md5,072da7c64c1dbe21a4d71c560431b96e", + "zarr.json:md5,73b113cb0e0dd3c688b7221003ce7062", + "zarr.json:md5,cb8cd70a8b5142c2bdcc65261f52013e", + "0:md5,adbe51c450a9a6c9086df9ebe220eefb", + "1:md5,7bb4eecf9fe3996a9b118082152a8750", + "zarr.json:md5,5ad6b55efbf7ea079dc73f60af2a12cb", + "0:md5,498463ef4033f523f8174237454f5dc7", + "zarr.json:md5,cf8da18bcd5b9661012f3b120d64d5d9", + "0:md5,a73b0d9b8919bb729c6eee8cf6632cd8", + "zarr.json:md5,a03571ade1e696e2e855ed06331acd09", + "0:md5,9efe1a6b6f8d3548f3ebdeab07e2e8b3", + "zarr.json:md5,be459dc60bef9886df3f9e6895430f6b", + "0:md5,4cd5ff674991828d1536bf85d47bcb0a", + "zarr.json:md5,73b113cb0e0dd3c688b7221003ce7062", + "zarr.json:md5,83ce0ad0ef9fc5f27b0c737f7bc2c88f", + "zarr.json:md5,0fc99ee9a39262fbd42f104d79a9edd8", "0:md5,397dc21e479c58fd2ffcbe6c3e627683", "1:md5,f1a6b9238c3095bad7867b22f293e5e6", "zarr.json:md5,893b4d8873fc997d364288c63ef0871f", @@ -329,6 +571,31 @@ "0:md5,4890e52e8f0efc8f560bc51199d5071c", "zarr.json:md5,1f4482a1e3b6e9ab30623b0ad12f5213", "zarr.json:md5,b98263d3964a5236145ccf6c808f8919", + "0:md5,9fbd82d28a84dddd52df1b055c00d9b0", + "1:md5,3519dd1d29e589a2f0e87c2e0a20586b", + "zarr.json:md5,5ad6b55efbf7ea079dc73f60af2a12cb", + "0:md5,500e366b93160eaf9cf5ee5dcd8fdd18", + "zarr.json:md5,cf8da18bcd5b9661012f3b120d64d5d9", + "0:md5,264bcf6f0679d8cab53930b5182aa2ac", + "zarr.json:md5,a03571ade1e696e2e855ed06331acd09", + "0:md5,c416a29978b65ccec08ad8e0d3e1dcf8", + "zarr.json:md5,be459dc60bef9886df3f9e6895430f6b", + "0:md5,072da7c64c1dbe21a4d71c560431b96e", + "zarr.json:md5,73b113cb0e0dd3c688b7221003ce7062", + "zarr.json:md5,cb8cd70a8b5142c2bdcc65261f52013e", + "0:md5,adbe51c450a9a6c9086df9ebe220eefb", + "1:md5,7bb4eecf9fe3996a9b118082152a8750", + "zarr.json:md5,5ad6b55efbf7ea079dc73f60af2a12cb", + "0:md5,498463ef4033f523f8174237454f5dc7", + "zarr.json:md5,cf8da18bcd5b9661012f3b120d64d5d9", + "0:md5,a73b0d9b8919bb729c6eee8cf6632cd8", + "zarr.json:md5,a03571ade1e696e2e855ed06331acd09", + "0:md5,9efe1a6b6f8d3548f3ebdeab07e2e8b3", + "zarr.json:md5,be459dc60bef9886df3f9e6895430f6b", + "0:md5,4cd5ff674991828d1536bf85d47bcb0a", + "zarr.json:md5,73b113cb0e0dd3c688b7221003ce7062", + "zarr.json:md5,83ce0ad0ef9fc5f27b0c737f7bc2c88f", + "zarr.json:md5,0fc99ee9a39262fbd42f104d79a9edd8", "0:md5,397dc21e479c58fd2ffcbe6c3e627683", "1:md5,f1a6b9238c3095bad7867b22f293e5e6", "zarr.json:md5,893b4d8873fc997d364288c63ef0871f", @@ -343,7 +610,7 @@ "zarr.json:md5,b98263d3964a5236145ccf6c808f8919" ] ], - "timestamp": "2026-08-04T14:29:47.103357166", + "timestamp": "2026-08-07T17:06:03.98293334", "meta": { "nf-test": "0.9.5", "nextflow": "26.04.6" @@ -374,7 +641,7 @@ "untar": 1.34 }, "Workflow": { - "nf-core/spatialaxe": "v1.0.1" + "nf-core/spatialaxe": "v1.0.1dev" }, "XENIUMRANGER_IMPORTSEGMENTATION": { "xeniumranger": "4.0.1.1" @@ -467,7 +734,7 @@ "fake_file.txt:md5,d41d8cd98f00b204e9800998ecf8427e" ] ], - "timestamp": "2026-08-04T14:30:03.467198601", + "timestamp": "2026-08-07T17:06:22.496109056", "meta": { "nf-test": "0.9.5", "nextflow": "26.04.6" diff --git a/tests/default.nf.test.snap b/tests/default.nf.test.snap index 840d3c89..adee3eca 100644 --- a/tests/default.nf.test.snap +++ b/tests/default.nf.test.snap @@ -24,7 +24,7 @@ "untar": 1.34 }, "Workflow": { - "nf-core/spatialaxe": "v1.0.1" + "nf-core/spatialaxe": "v1.0.1dev" }, "XENIUMRANGER_IMPORTSEGMENTATION": { "xeniumranger": "4.0.1.1" @@ -111,38 +111,13 @@ ".stub:md5,d41d8cd98f00b204e9800998ecf8427e", ".stub:md5,d41d8cd98f00b204e9800998ecf8427e", "multiqc_report.html:md5,d41d8cd98f00b204e9800998ecf8427e", - "cell-polygons.geojson:md5,d41d8cd98f00b204e9800998ecf8427e", - "transcript-metadata.csv:md5,d41d8cd98f00b204e9800998ecf8427e", "fake_file.txt:md5,d41d8cd98f00b204e9800998ecf8427e", "fake_file.txt:md5,d41d8cd98f00b204e9800998ecf8427e", "fake_file.txt:md5,d41d8cd98f00b204e9800998ecf8427e", - "fake_file.txt:md5,d41d8cd98f00b204e9800998ecf8427e", - ".end-of-run:md5,d41d8cd98f00b204e9800998ecf8427e", - "analysis.tar.gz:md5,d41d8cd98f00b204e9800998ecf8427e", - "analysis.zarr.zip:md5,d41d8cd98f00b204e9800998ecf8427e", - "analysis_summary.html:md5,d41d8cd98f00b204e9800998ecf8427e", - "aux_outputs.tar.gz:md5,d41d8cd98f00b204e9800998ecf8427e", - "cell_boundaries.csv.gz:md5,d41d8cd98f00b204e9800998ecf8427e", - "cell_boundaries.parquet:md5,d41d8cd98f00b204e9800998ecf8427e", - "cell_feature_matrix.h5:md5,d41d8cd98f00b204e9800998ecf8427e", - "cell_feature_matrix.tar.gz:md5,d41d8cd98f00b204e9800998ecf8427e", - "cell_feature_matrix.zarr.zip:md5,d41d8cd98f00b204e9800998ecf8427e", - "cells.csv.gz:md5,d41d8cd98f00b204e9800998ecf8427e", - "cells.parquet:md5,d41d8cd98f00b204e9800998ecf8427e", - "cells.zarr.zip:md5,d41d8cd98f00b204e9800998ecf8427e", - "experiment.xenium:md5,d41d8cd98f00b204e9800998ecf8427e", - "gene_panel.json:md5,d41d8cd98f00b204e9800998ecf8427e", - "metrics_summary.csv:md5,d41d8cd98f00b204e9800998ecf8427e", - "morphology.ome.tif:md5,d41d8cd98f00b204e9800998ecf8427e", - "morphology_focus_0000.ome.tif:md5,d41d8cd98f00b204e9800998ecf8427e", - "nucleus_boundaries.csv.gz:md5,d41d8cd98f00b204e9800998ecf8427e", - "nucleus_boundaries.parquet:md5,d41d8cd98f00b204e9800998ecf8427e", - "transcripts.parquet:md5,d41d8cd98f00b204e9800998ecf8427e", - "transcripts.zarr.zip:md5,d41d8cd98f00b204e9800998ecf8427e", - "experiment.xenium:md5,d41d8cd98f00b204e9800998ecf8427e" + "fake_file.txt:md5,d41d8cd98f00b204e9800998ecf8427e" ] ], - "timestamp": "2026-07-28T12:20:41.663589777", + "timestamp": "2026-08-07T17:01:16.476317223", "meta": { "nf-test": "0.9.5", "nextflow": "26.04.6" diff --git a/tests/image_mode.nf.test.snap b/tests/image_mode.nf.test.snap index 129ba64b..6df2cf6d 100644 --- a/tests/image_mode.nf.test.snap +++ b/tests/image_mode.nf.test.snap @@ -30,7 +30,7 @@ "untar": 1.34 }, "Workflow": { - "nf-core/spatialaxe": "v1.0.1" + "nf-core/spatialaxe": "v1.0.1dev" }, "XENIUMRANGER_IMPORTSEGMENTATION": { "xeniumranger": "4.0.1.1" @@ -155,7 +155,7 @@ "resized_morphology_focus_0000.ome_cp_masks.tif.tif:md5,d41d8cd98f00b204e9800998ecf8427e" ] ], - "timestamp": "2026-08-04T14:37:29.360791601", + "timestamp": "2026-08-07T17:07:07.950261954", "meta": { "nf-test": "0.9.5", "nextflow": "26.04.6" diff --git a/tests/preview_mode.nf.test.snap b/tests/preview_mode.nf.test.snap index cc286151..3282ecee 100644 --- a/tests/preview_mode.nf.test.snap +++ b/tests/preview_mode.nf.test.snap @@ -18,7 +18,7 @@ "untar": 1.34 }, "Workflow": { - "nf-core/spatialaxe": "v1.0.1" + "nf-core/spatialaxe": "v1.0.1dev" } }, [ @@ -111,7 +111,7 @@ "umap_mqc.tsv:md5,d41d8cd98f00b204e9800998ecf8427e" ] ], - "timestamp": "2026-07-23T15:57:29.316600844", + "timestamp": "2026-08-07T17:07:37.796900605", "meta": { "nf-test": "0.9.5", "nextflow": "26.04.6" diff --git a/tests/segfree_mode.nf.test.snap b/tests/segfree_mode.nf.test.snap index 90a83476..22848328 100644 --- a/tests/segfree_mode.nf.test.snap +++ b/tests/segfree_mode.nf.test.snap @@ -12,7 +12,7 @@ "untar": 1.34 }, "Workflow": { - "nf-core/spatialaxe": "v1.0.1" + "nf-core/spatialaxe": "v1.0.1dev" } }, [ @@ -88,7 +88,7 @@ "transcripts.zarr.zip:md5,d41d8cd98f00b204e9800998ecf8427e" ] ], - "timestamp": "2026-07-23T15:57:58.134363385", + "timestamp": "2026-08-07T17:08:08.760478641", "meta": { "nf-test": "0.9.5", "nextflow": "26.04.6" diff --git a/tests/spoqc.nf.test b/tests/spoqc.nf.test new file mode 100644 index 00000000..1e28b0ec --- /dev/null +++ b/tests/spoqc.nf.test @@ -0,0 +1,92 @@ +nextflow_pipeline { + + name "Test pipeline - runs the default pipeline tests (coordinate mode)" + script "../main.nf" + tag "pipeline" + config "../conf/tests/test_spoqc.config" + + test("-profile test") { + + when { + params { + outdir = "$outputDir" + } + } + + then { + // stable_name: All files + folders in ${params.outdir}/ with a stable name + // spoqc/** is excluded: spoQC generates thousands of non-deterministic + // plot/report files, so only folder existence is checked below instead + def stable_name = getAllFilesFromDir(params.outdir, relative: true, includeDir: true, ignore: ['pipeline_info/*.{html,json,txt}', 'spoqc/**']) + // stable_path: All files in ${params.outdir}/ with stable content + def stable_path = getAllFilesFromDir(params.outdir, ignoreFile: 'tests/.nftignore') + // spoQC report subfolders: checked for existence only (see spoqc/** exclusion above) + def spoqc_report_dirs = [ + "ambientqc", "analysis", "annotation", "bubbleqc", "cellcycleqc", + "cellqc", "combine_masks", "doubletqc", "generalqc", "hqcr", + "hqpr", "hqtr", "modelqc", "transcriptqc", "voidqc", "whole_slide_qc" + ] + assertAll( + { assert workflow.success}, + { assert snapshot( + // pipeline versions.yml file for multiqc from which Nextflow version is removed because we test pipelines on multiple Nextflow versions + removeNextflowVersion("$outputDir/pipeline_info/nf_core_spatialaxe_software_mqc_versions.yml"), + // All stable path name, with a relative path + stable_name, + // All files with stable contents + stable_path + ).match() }, + { assert new File("$outputDir/spoqc/test_run/report").isDirectory() }, + { assert new File("$outputDir/spoqc/test_run/report/report.html").exists() }, + { assert new File("$outputDir/spoqc/test_run/spoQC_tmp").isDirectory() }, + *spoqc_report_dirs.collect { dir -> + { -> assert new File("$outputDir/spoqc/test_run/report/$dir").isDirectory() } + } + ) + } + } + + + test("-profile test stub") { + + options "-stub" + + when { + params { + outdir = "$outputDir" + } + } + + then { + // stable_name: All files + folders in ${params.outdir}/ with a stable name + // spoqc/** is excluded: spoQC generates thousands of non-deterministic + // plot/report files, so only folder existence is checked below instead + def stable_name = getAllFilesFromDir(params.outdir, relative: true, includeDir: true, ignore: ['pipeline_info/*.{html,json,txt}', 'spoqc/**']) + // stable_path: All files in ${params.outdir}/ with stable content + def stable_path = getAllFilesFromDir(params.outdir, ignoreFile: 'tests/.nftignore') + // spoQC report subfolders: checked for existence only (see spoqc/** exclusion above) + def spoqc_report_dirs = [ + "ambientqc", "analysis", "annotation", "bubbleqc", "cellcycleqc", + "cellqc", "combine_masks", "doubletqc", "generalqc", "hqcr", + "hqpr", "hqtr", "modelqc", "transcriptqc", "voidqc", "whole_slide_qc" + ] + assertAll( + { assert workflow.success}, + { assert snapshot( + // pipeline versions.yml file for multiqc from which Nextflow version is removed because we test pipelines on multiple Nextflow versions + removeNextflowVersion("$outputDir/pipeline_info/nf_core_spatialaxe_software_mqc_versions.yml"), + // All stable path name, with a relative path + stable_name, + // All files with stable contents + stable_path + ).match() }, + { assert new File("$outputDir/spoqc/test_run/report").isDirectory() }, + { assert new File("$outputDir/spoqc/test_run/report/report.html").exists() }, + { assert new File("$outputDir/spoqc/test_run/spoQC_tmp").isDirectory() }, + *spoqc_report_dirs.collect { dir -> + { -> assert new File("$outputDir/spoqc/test_run/report/$dir").isDirectory() } + } + ) + } + } +} diff --git a/tests/spoqc.nf.test.snap b/tests/spoqc.nf.test.snap new file mode 100644 index 00000000..ae332213 --- /dev/null +++ b/tests/spoqc.nf.test.snap @@ -0,0 +1,747 @@ +{ + "-profile test": { + "content": [ + { + "SPATIALDATA_WRITE_RAW_BUNDLE": { + "spatialdata": "0.7.2" + }, + "SPOQC_AMBIENT": { + "spoqc": "0.1.0" + }, + "SPOQC_ANALYSIS_CATEGORY": { + "spoqc": "0.1.0" + }, + "SPOQC_ANALYSIS_CLUSTER": { + "spoqc": "0.1.0" + }, + "SPOQC_ANALYSIS_OVERVIEW": { + "spoqc": "0.1.0" + }, + "SPOQC_ANNOTATION": { + "spoqc": "0.1.0" + }, + "SPOQC_BUBBLE": { + "spoqc": "0.1.0" + }, + "SPOQC_CELL": { + "spoqc": "0.1.0" + }, + "SPOQC_CELLCYCLE": { + "spoqc": "0.1.0" + }, + "SPOQC_COMBINE_MASKS": { + "spoqc": "0.1.0" + }, + "SPOQC_DOUBLET": { + "spoqc": "0.1.0" + }, + "SPOQC_FINALREPORT": { + "spoqc": "0.1.0" + }, + "SPOQC_GENERAL": { + "spoqc": "0.1.0" + }, + "SPOQC_HQCR_CELLTYPE": { + "spoqc": "0.1.0" + }, + "SPOQC_HQCR_IDENT": { + "spoqc": "0.1.0" + }, + "SPOQC_HQPR_BOUNDING_BOX": { + "spoqc": "0.1.0" + }, + "SPOQC_HQPR_CELLTYPE": { + "spoqc": "0.1.0" + }, + "SPOQC_HQPR_CLUSTERING": { + "spoqc": "0.1.0" + }, + "SPOQC_HQPR_METRICES": { + "spoqc": "0.1.0" + }, + "SPOQC_HQPR_REFINEMENT": { + "spoqc": "0.1.0" + }, + "SPOQC_HQTR_AC": { + "spoqc": "0.1.0" + }, + "SPOQC_HQTR_BOUNDING_BOX": { + "spoqc": "0.1.0" + }, + "SPOQC_HQTR_CELLTYPE": { + "spoqc": "0.1.0" + }, + "SPOQC_HQTR_CLUSTERING": { + "spoqc": "0.1.0" + }, + "SPOQC_HQTR_METRICES": { + "spoqc": "0.1.0" + }, + "SPOQC_HQTR_QV": { + "spoqc": "0.1.0" + }, + "SPOQC_HQTR_REFINEMENT": { + "spoqc": "0.1.0" + }, + "SPOQC_MODEL": { + "spoqc": "0.1.0" + }, + "SPOQC_TRANSCRIPT": { + "spoqc": "0.1.0" + }, + "SPOQC_VOID": { + "spoqc": "0.1.0" + }, + "SPOQC_WHOLE_SLIDE": { + "spoqc": "0.1.0" + }, + "UNTAR": { + "untar": 1.34 + }, + "Workflow": { + "nf-core/spatialaxe": "v1.0.1dev" + } + }, + [ + "pipeline_info", + "pipeline_info/nf_core_spatialaxe_software_mqc_versions.yml", + "qc", + "qc/multiqc", + "qc/multiqc/multiqc_data", + "qc/multiqc/multiqc_data/llms-full.txt", + "qc/multiqc/multiqc_data/multiqc.log", + "qc/multiqc/multiqc_data/multiqc.parquet", + "qc/multiqc/multiqc_data/multiqc_citations.txt", + "qc/multiqc/multiqc_data/multiqc_data.json", + "qc/multiqc/multiqc_data/multiqc_general_stats.txt", + "qc/multiqc/multiqc_data/multiqc_software_versions.txt", + "qc/multiqc/multiqc_data/multiqc_sources.txt", + "qc/multiqc/multiqc_data/multiqc_xenium.txt", + "qc/multiqc/multiqc_data/xenium_fov_quality_ranges.txt", + "qc/multiqc/multiqc_data/xenium_segmentation.txt", + "qc/multiqc/multiqc_data/xenium_transcript_quality_per_sample_table.txt", + "qc/multiqc/multiqc_data/xenium_transcripts_per_gene.txt", + "qc/multiqc/multiqc_plots", + "qc/multiqc/multiqc_plots/pdf", + "qc/multiqc/multiqc_plots/pdf/xenium_fov_quality_ranges-cnt.pdf", + "qc/multiqc/multiqc_plots/pdf/xenium_fov_quality_ranges-pct.pdf", + "qc/multiqc/multiqc_plots/pdf/xenium_segmentation.pdf", + "qc/multiqc/multiqc_plots/pdf/xenium_transcript_quality_per_sample_table.pdf", + "qc/multiqc/multiqc_plots/pdf/xenium_transcripts_per_gene.pdf", + "qc/multiqc/multiqc_plots/png", + "qc/multiqc/multiqc_plots/png/xenium_fov_quality_ranges-cnt.png", + "qc/multiqc/multiqc_plots/png/xenium_fov_quality_ranges-pct.png", + "qc/multiqc/multiqc_plots/png/xenium_segmentation.png", + "qc/multiqc/multiqc_plots/png/xenium_transcript_quality_per_sample_table.png", + "qc/multiqc/multiqc_plots/png/xenium_transcripts_per_gene.png", + "qc/multiqc/multiqc_plots/svg", + "qc/multiqc/multiqc_plots/svg/xenium_fov_quality_ranges-cnt.svg", + "qc/multiqc/multiqc_plots/svg/xenium_fov_quality_ranges-pct.svg", + "qc/multiqc/multiqc_plots/svg/xenium_segmentation.svg", + "qc/multiqc/multiqc_plots/svg/xenium_transcript_quality_per_sample_table.svg", + "qc/multiqc/multiqc_plots/svg/xenium_transcripts_per_gene.svg", + "qc/multiqc/multiqc_report.html", + "qc/spatialdata", + "qc/spatialdata/write", + "qc/spatialdata/write/spatialdata", + "qc/spatialdata/write/spatialdata/test_run", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/images", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/images/morphology_focus", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/images/morphology_focus/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/images/morphology_focus/0/c", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/images/morphology_focus/0/c/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/images/morphology_focus/0/c/0/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/images/morphology_focus/0/c/0/0/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/images/morphology_focus/0/c/0/0/1", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/images/morphology_focus/0/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/images/morphology_focus/1", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/images/morphology_focus/1/c", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/images/morphology_focus/1/c/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/images/morphology_focus/1/c/0/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/images/morphology_focus/1/c/0/0/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/images/morphology_focus/1/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/images/morphology_focus/2", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/images/morphology_focus/2/c", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/images/morphology_focus/2/c/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/images/morphology_focus/2/c/0/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/images/morphology_focus/2/c/0/0/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/images/morphology_focus/2/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/images/morphology_focus/3", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/images/morphology_focus/3/c", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/images/morphology_focus/3/c/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/images/morphology_focus/3/c/0/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/images/morphology_focus/3/c/0/0/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/images/morphology_focus/3/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/images/morphology_focus/4", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/images/morphology_focus/4/c", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/images/morphology_focus/4/c/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/images/morphology_focus/4/c/0/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/images/morphology_focus/4/c/0/0/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/images/morphology_focus/4/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/images/morphology_focus/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/images/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/0/c", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/0/c/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/0/c/0/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/0/c/0/1", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/0/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/1", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/1/c", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/1/c/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/1/c/0/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/1/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/2", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/2/c", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/2/c/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/2/c/0/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/2/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/3", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/3/c", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/3/c/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/3/c/0/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/3/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/4", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/4/c", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/4/c/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/4/c/0/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/4/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/cell_labels/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/0/c", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/0/c/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/0/c/0/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/0/c/0/1", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/0/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/1", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/1/c", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/1/c/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/1/c/0/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/1/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/2", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/2/c", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/2/c/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/2/c/0/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/2/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/3", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/3/c", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/3/c/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/3/c/0/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/3/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/4", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/4/c", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/4/c/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/4/c/0/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/4/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/nucleus_labels/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/labels/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/points", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/points/transcripts", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/points/transcripts/points.parquet", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/points/transcripts/points.parquet/part.0.parquet", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/points/transcripts/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/points/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/shapes", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/shapes/cell_boundaries", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/shapes/cell_boundaries/shapes.parquet", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/shapes/cell_boundaries/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/shapes/cell_circles", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/shapes/cell_circles/shapes.parquet", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/shapes/cell_circles/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/shapes/nucleus_boundaries", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/shapes/nucleus_boundaries/shapes.parquet", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/shapes/nucleus_boundaries/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/shapes/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/X", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/X/data", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/X/data/c", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/X/data/c/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/X/data/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/X/indices", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/X/indices/c", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/X/indices/c/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/X/indices/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/X/indptr", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/X/indptr/c", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/X/indptr/c/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/X/indptr/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/X/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/layers", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/layers/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/_index", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/_index/c", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/_index/c/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/_index/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/cell_area", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/cell_area/c", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/cell_area/c/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/cell_area/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/cell_id", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/cell_id/c", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/cell_id/c/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/cell_id/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/cell_labels", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/cell_labels/c", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/cell_labels/c/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/cell_labels/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/control_codeword_counts", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/control_codeword_counts/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/control_probe_counts", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/control_probe_counts/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/deprecated_codeword_counts", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/deprecated_codeword_counts/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/genomic_control_counts", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/genomic_control_counts/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/nucleus_area", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/nucleus_area/c", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/nucleus_area/c/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/nucleus_area/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/nucleus_count", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/nucleus_count/c", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/nucleus_count/c/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/nucleus_count/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/region", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/region/categories", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/region/categories/c", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/region/categories/c/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/region/categories/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/region/codes", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/region/codes/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/region/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/segmentation_method", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/segmentation_method/c", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/segmentation_method/c/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/segmentation_method/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/total_counts", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/total_counts/c", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/total_counts/c/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/total_counts/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/transcript_counts", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/transcript_counts/c", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/transcript_counts/c/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/transcript_counts/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/unassigned_codeword_counts", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/unassigned_codeword_counts/c", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/unassigned_codeword_counts/c/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/unassigned_codeword_counts/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/z_level", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/z_level/c", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/z_level/c/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/z_level/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obs/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obsm", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obsm/spatial", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obsm/spatial/c", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obsm/spatial/c/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obsm/spatial/c/0/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obsm/spatial/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obsm/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obsp", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/obsp/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/raw", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/raw/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/uns", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/uns/spatialdata_attrs", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/uns/spatialdata_attrs/instance_key", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/uns/spatialdata_attrs/instance_key/c", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/uns/spatialdata_attrs/instance_key/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/uns/spatialdata_attrs/region", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/uns/spatialdata_attrs/region/c", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/uns/spatialdata_attrs/region/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/uns/spatialdata_attrs/region_key", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/uns/spatialdata_attrs/region_key/c", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/uns/spatialdata_attrs/region_key/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/uns/spatialdata_attrs/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/uns/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/var", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/var/_index", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/var/_index/c", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/var/_index/c/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/var/_index/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/var/feature_types", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/var/feature_types/c", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/var/feature_types/c/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/var/feature_types/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/var/gene_ids", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/var/gene_ids/c", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/var/gene_ids/c/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/var/gene_ids/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/var/genome", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/var/genome/c", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/var/genome/c/0", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/var/genome/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/var/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/varm", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/varm/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/varp", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/varp/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/table/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/tables/zarr.json", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/zarr.json", + "qc/untar", + "qc/untar/test_bundle", + "qc/untar/test_bundle/.end-of-run", + "qc/untar/test_bundle/analysis.tar.gz", + "qc/untar/test_bundle/analysis.zarr.zip", + "qc/untar/test_bundle/analysis_summary.html", + "qc/untar/test_bundle/aux_outputs.tar.gz", + "qc/untar/test_bundle/cell_boundaries.csv.gz", + "qc/untar/test_bundle/cell_boundaries.parquet", + "qc/untar/test_bundle/cell_feature_matrix.h5", + "qc/untar/test_bundle/cell_feature_matrix.tar.gz", + "qc/untar/test_bundle/cell_feature_matrix.zarr.zip", + "qc/untar/test_bundle/cells.csv.gz", + "qc/untar/test_bundle/cells.parquet", + "qc/untar/test_bundle/cells.zarr.zip", + "qc/untar/test_bundle/experiment.xenium", + "qc/untar/test_bundle/gene_panel.json", + "qc/untar/test_bundle/metrics_summary.csv", + "qc/untar/test_bundle/morphology.ome.tif", + "qc/untar/test_bundle/morphology_focus", + "qc/untar/test_bundle/morphology_focus/morphology_focus_0000.ome.tif", + "qc/untar/test_bundle/nucleus_boundaries.csv.gz", + "qc/untar/test_bundle/nucleus_boundaries.parquet", + "qc/untar/test_bundle/transcripts.parquet", + "qc/untar/test_bundle/transcripts.zarr.zip", + "spoqc" + ], + [ + "multiqc_citations.txt:md5,4c806e63a283ec1b7e78cdae3a923d4f", + "multiqc_general_stats.txt:md5,4f529748a8d30e5672ed3058ae59fb41", + "multiqc_software_versions.txt:md5,1774c1ba6c0b28109544da460f02aad4", + "multiqc_xenium.txt:md5,0ee9673aeaa39570a860f79759c62981", + "xenium_fov_quality_ranges.txt:md5,4ac36f9b265e6c76e86045875d316cf6", + "xenium_segmentation.txt:md5,171e5f89e5dd0a3def24030996dc5732", + "xenium_transcript_quality_per_sample_table.txt:md5,d48da999968828f168855048d081c799", + "xenium_transcripts_per_gene.txt:md5,b46b3c414bc660e03e3df0345dd5e3e2", + "0:md5,397dc21e479c58fd2ffcbe6c3e627683", + "1:md5,f1a6b9238c3095bad7867b22f293e5e6", + "zarr.json:md5,893b4d8873fc997d364288c63ef0871f", + "0:md5,519d79fe18256d530a2070e31925fac6", + "zarr.json:md5,9e0748b559911795c141000a77ab7e59", + "0:md5,f349fb7934242be4cf4ac28e1a7e18f4", + "zarr.json:md5,32b707be767bd056085d0f349d3ab68c", + "0:md5,000e609e16f4975474a7d57eed232daa", + "zarr.json:md5,4690fb68f05cbca9148144c85bd9687f", + "0:md5,4890e52e8f0efc8f560bc51199d5071c", + "zarr.json:md5,1f4482a1e3b6e9ab30623b0ad12f5213", + "zarr.json:md5,b98263d3964a5236145ccf6c808f8919", + "0:md5,9fbd82d28a84dddd52df1b055c00d9b0", + "1:md5,3519dd1d29e589a2f0e87c2e0a20586b", + "zarr.json:md5,5ad6b55efbf7ea079dc73f60af2a12cb", + "0:md5,500e366b93160eaf9cf5ee5dcd8fdd18", + "zarr.json:md5,cf8da18bcd5b9661012f3b120d64d5d9", + "0:md5,264bcf6f0679d8cab53930b5182aa2ac", + "zarr.json:md5,a03571ade1e696e2e855ed06331acd09", + "0:md5,c416a29978b65ccec08ad8e0d3e1dcf8", + "zarr.json:md5,be459dc60bef9886df3f9e6895430f6b", + "0:md5,072da7c64c1dbe21a4d71c560431b96e", + "zarr.json:md5,73b113cb0e0dd3c688b7221003ce7062", + "zarr.json:md5,cb8cd70a8b5142c2bdcc65261f52013e", + "0:md5,adbe51c450a9a6c9086df9ebe220eefb", + "1:md5,7bb4eecf9fe3996a9b118082152a8750", + "zarr.json:md5,5ad6b55efbf7ea079dc73f60af2a12cb", + "0:md5,498463ef4033f523f8174237454f5dc7", + "zarr.json:md5,cf8da18bcd5b9661012f3b120d64d5d9", + "0:md5,a73b0d9b8919bb729c6eee8cf6632cd8", + "zarr.json:md5,a03571ade1e696e2e855ed06331acd09", + "0:md5,9efe1a6b6f8d3548f3ebdeab07e2e8b3", + "zarr.json:md5,be459dc60bef9886df3f9e6895430f6b", + "0:md5,4cd5ff674991828d1536bf85d47bcb0a", + "zarr.json:md5,73b113cb0e0dd3c688b7221003ce7062", + "zarr.json:md5,83ce0ad0ef9fc5f27b0c737f7bc2c88f", + "zarr.json:md5,0fc99ee9a39262fbd42f104d79a9edd8", + "part.0.parquet:md5,cf65a556bf8961136ef60c48918ffb85", + "zarr.json:md5,de25227672526995ee814db4cdf6b00e", + "zarr.json:md5,457126c0639af2eba0140851c39c1aad", + "shapes.parquet:md5,034188401e9a890a058251949bdf6c0f", + "zarr.json:md5,a2becb83a1a5791c0b6f2aeeadaebf5b", + "shapes.parquet:md5,4073f161ac78fc49bda5c10dc8643c74", + "zarr.json:md5,a2becb83a1a5791c0b6f2aeeadaebf5b", + "shapes.parquet:md5,aa4ee3eaecae7f52a66241e5c82835c2", + "zarr.json:md5,a2becb83a1a5791c0b6f2aeeadaebf5b", + "zarr.json:md5,457126c0639af2eba0140851c39c1aad", + "0:md5,dbbe04ffd6b2db2b2db1216c98e662f2", + "zarr.json:md5,93d2277b7e39c627891b967db7a9f0a3", + "0:md5,22cb401dbf634df13b03b7853db2e538", + "zarr.json:md5,a3ec31859f46b3df2a866e82c40f026c", + "0:md5,a1f07b130822960b0562ddc4ec95d31b", + "zarr.json:md5,949a320badfe72bd7cc4e7e827d16158", + "zarr.json:md5,e2d6b9efe2a9191944f3f113e9055919", + "zarr.json:md5,445840b3cc4c9e66c9e53b5268c8ae0f", + "0:md5,86097965e1a6c78250d3706c1ffaca71", + "zarr.json:md5,d6fae5a10079c1e081f432ad9565cc58", + "0:md5,fcaa2334d71dbdc4a3081e85c5dfcc6f", + "zarr.json:md5,6c2f9a7cea645cc6aebbaa0a7b918dac", + "0:md5,c3386552bba6519f18376485c01234e0", + "zarr.json:md5,d6fae5a10079c1e081f432ad9565cc58", + "0:md5,bac3d1932457949068c6bf1d8a2fe329", + "zarr.json:md5,2c77f577e929a75870f0ebcb89795739", + "zarr.json:md5,2c77f577e929a75870f0ebcb89795739", + "zarr.json:md5,2c77f577e929a75870f0ebcb89795739", + "zarr.json:md5,2c77f577e929a75870f0ebcb89795739", + "zarr.json:md5,2c77f577e929a75870f0ebcb89795739", + "0:md5,a7faa9c6e12d1df43974815dac35f08c", + "zarr.json:md5,6c2f9a7cea645cc6aebbaa0a7b918dac", + "0:md5,4a043d7128d4c6ae94cf49d6f703aee5", + "zarr.json:md5,6c2f9a7cea645cc6aebbaa0a7b918dac", + "0:md5,6bbd32254e2b288dfa3aaabecdb38d97", + "zarr.json:md5,50031b437386f169b150fcba12866de7", + "zarr.json:md5,daecaa428e9034ea0c89071d2cba35df", + "zarr.json:md5,70af6515e02956e810d5146bac7330a0", + "0:md5,1df99fb2f49943d3fab1bb683cd5cd60", + "zarr.json:md5,d6fae5a10079c1e081f432ad9565cc58", + "0:md5,3e0a1e4ac73f60b265d3ef5c56b3904d", + "zarr.json:md5,2c77f577e929a75870f0ebcb89795739", + "0:md5,e9872e06532aea0a3c81bf14f449c826", + "zarr.json:md5,2c77f577e929a75870f0ebcb89795739", + "0:md5,75e416918b858b0dcfdea9f4178412a3", + "zarr.json:md5,2c77f577e929a75870f0ebcb89795739", + "0:md5,c98261c5bcd442aa47c031919548c865", + "zarr.json:md5,6c2f9a7cea645cc6aebbaa0a7b918dac", + "zarr.json:md5,ffa73d17acf806eaf46fa74dd32d6c67", + "0:md5,7fb75d1d95278302c8b127c25b37deb5", + "zarr.json:md5,742521b0a44e146d0ac873deda2b268e", + "zarr.json:md5,445840b3cc4c9e66c9e53b5268c8ae0f", + "zarr.json:md5,445840b3cc4c9e66c9e53b5268c8ae0f", + "zarr.json:md5,cd8801388fec0f2eadf1e4e5b1351b53", + "c:md5,e0aeeee78622132ce63fdfeabe0784ff", + "zarr.json:md5,6b4ae7d978cf2ffbceda9a4a823fe3cb", + "c:md5,6bbd32254e2b288dfa3aaabecdb38d97", + "zarr.json:md5,6b4ae7d978cf2ffbceda9a4a823fe3cb", + "c:md5,557b0ae11044ce7360261e0fbe05ac5c", + "zarr.json:md5,6b4ae7d978cf2ffbceda9a4a823fe3cb", + "zarr.json:md5,445840b3cc4c9e66c9e53b5268c8ae0f", + "zarr.json:md5,445840b3cc4c9e66c9e53b5268c8ae0f", + "0:md5,92b24891bbe89b3fa800917e2e8a161c", + "zarr.json:md5,9437160e1e2854ec02195223137e3e54", + "0:md5,542f34f2836188eb9b26f32748b3f6d0", + "zarr.json:md5,9437160e1e2854ec02195223137e3e54", + "0:md5,66bc395f6b1c5495d8755572402955fe", + "zarr.json:md5,9437160e1e2854ec02195223137e3e54", + "0:md5,25ab545b58927d101261a72f4cb9fffc", + "zarr.json:md5,9437160e1e2854ec02195223137e3e54", + "zarr.json:md5,fec3aa7671cc196ec026098cc8f693e6", + "zarr.json:md5,445840b3cc4c9e66c9e53b5268c8ae0f", + "zarr.json:md5,445840b3cc4c9e66c9e53b5268c8ae0f", + "zarr.json:md5,7dfc5bbc0295657153fe8ad21012a22b", + "zarr.json:md5,457126c0639af2eba0140851c39c1aad", + "zarr.json:md5,6d8090ababdd8ca48dafe0fc11d1982f", + ".end-of-run:md5,d41d8cd98f00b204e9800998ecf8427e", + "analysis.tar.gz:md5,06b7c89909ddf3149b0debcada5dc627", + "analysis.zarr.zip:md5,1ffb1b86586fe6c80ce1676b79137785", + "analysis_summary.html:md5,e91cdd23848a81b1a553b40e09d994de", + "aux_outputs.tar.gz:md5,aac93b35fbb052302042a044629eec56", + "cell_boundaries.csv.gz:md5,8b4f2aa455a6fb14b2669a42db32ea7e", + "cell_boundaries.parquet:md5,e55d6a7fbec336103994baad8c8e4a9a", + "cell_feature_matrix.h5:md5,96cb400f1b1dd6f8796daea0ad5c74e6", + "cell_feature_matrix.tar.gz:md5,16b63c6c11d6411738019314bcc8113c", + "cell_feature_matrix.zarr.zip:md5,36f45a290cf4ee1232f2d1cd0fdbd820", + "cells.csv.gz:md5,3cef2d7cc8cfba1d47bdb7c65c3d5d5f", + "cells.parquet:md5,e1450c7eca3d7ce0d4911c95042b1303", + "cells.zarr.zip:md5,556e47d5b14150239b10b2f801defa2b", + "experiment.xenium:md5,eb70039966ed3ed8eefc0f3fcf4c72f6", + "gene_panel.json:md5,8890dd5fd90706e751554ac3fdfdedde", + "metrics_summary.csv:md5,e0928a0c20e7b34162f02670ed712dde", + "morphology.ome.tif:md5,6b65fff28a38a001b8f25061737fbf9b", + "morphology_focus_0000.ome.tif:md5,90e796ad634d14e62cf2ebcadf2eaf98", + "nucleus_boundaries.csv.gz:md5,e417b6e293298870956d42c7106cbd0c", + "nucleus_boundaries.parquet:md5,bacbfc3c2e956d899e1d8ccba5dd7c5e", + "transcripts.parquet:md5,203cb05ee7689805cc505ebda9557551", + "transcripts.zarr.zip:md5,807e63a2ef8340e085cd899507f45395" + ] + ], + "timestamp": "2026-08-13T16:08:30.523372022", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + }, + "-profile test stub": { + "content": [ + { + "SPATIALDATA_WRITE_RAW_BUNDLE": { + "spatialdata": "0.7.2" + }, + "SPOQC_AMBIENT": { + "spoqc": "0.1.0" + }, + "SPOQC_ANALYSIS_CATEGORY": { + "spoqc": "0.1.0" + }, + "SPOQC_ANALYSIS_CLUSTER": { + "spoqc": "0.1.0" + }, + "SPOQC_ANALYSIS_OVERVIEW": { + "spoqc": "0.1.0" + }, + "SPOQC_ANNOTATION": { + "spoqc": "0.1.0" + }, + "SPOQC_BUBBLE": { + "spoqc": "0.1.0" + }, + "SPOQC_CELL": { + "spoqc": "0.1.0" + }, + "SPOQC_CELLCYCLE": { + "spoqc": "0.1.0" + }, + "SPOQC_COMBINE_MASKS": { + "spoqc": "0.1.0" + }, + "SPOQC_DOUBLET": { + "spoqc": "0.1.0" + }, + "SPOQC_FINALREPORT": { + "spoqc": "0.1.0" + }, + "SPOQC_GENERAL": { + "spoqc": "0.1.0" + }, + "SPOQC_HQCR_CELLTYPE": { + "spoqc": "0.1.0" + }, + "SPOQC_HQCR_IDENT": { + "spoqc": "0.1.0" + }, + "SPOQC_HQPR_BOUNDING_BOX": { + "spoqc": "0.1.0" + }, + "SPOQC_HQPR_CELLTYPE": { + "spoqc": "0.1.0" + }, + "SPOQC_HQPR_CLUSTERING": { + "spoqc": "0.1.0" + }, + "SPOQC_HQPR_METRICES": { + "spoqc": "0.1.0" + }, + "SPOQC_HQPR_REFINEMENT": { + "spoqc": "0.1.0" + }, + "SPOQC_HQTR_AC": { + "spoqc": "0.1.0" + }, + "SPOQC_HQTR_BOUNDING_BOX": { + "spoqc": "0.1.0" + }, + "SPOQC_HQTR_CELLTYPE": { + "spoqc": "0.1.0" + }, + "SPOQC_HQTR_CLUSTERING": { + "spoqc": "0.1.0" + }, + "SPOQC_HQTR_METRICES": { + "spoqc": "0.1.0" + }, + "SPOQC_HQTR_QV": { + "spoqc": "0.1.0" + }, + "SPOQC_HQTR_REFINEMENT": { + "spoqc": "0.1.0" + }, + "SPOQC_MODEL": { + "spoqc": "0.1.0" + }, + "SPOQC_TRANSCRIPT": { + "spoqc": "0.1.0" + }, + "SPOQC_VOID": { + "spoqc": "0.1.0" + }, + "SPOQC_WHOLE_SLIDE": { + "spoqc": "0.1.0" + }, + "UNTAR": { + "untar": 1.34 + }, + "Workflow": { + "nf-core/spatialaxe": "v1.0.1dev" + } + }, + [ + "pipeline_info", + "pipeline_info/nf_core_spatialaxe_software_mqc_versions.yml", + "qc", + "qc/multiqc", + "qc/multiqc/multiqc_data", + "qc/multiqc/multiqc_data/.stub", + "qc/multiqc/multiqc_plots", + "qc/multiqc/multiqc_plots/.stub", + "qc/multiqc/multiqc_report.html", + "qc/spatialdata", + "qc/spatialdata/write", + "qc/spatialdata/write/spatialdata", + "qc/spatialdata/write/spatialdata/test_run", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle", + "qc/spatialdata/write/spatialdata/test_run/raw_bundle/fake_file.txt", + "qc/untar", + "qc/untar/test_bundle", + "qc/untar/test_bundle/.end-of-run", + "qc/untar/test_bundle/analysis.tar.gz", + "qc/untar/test_bundle/analysis.zarr.zip", + "qc/untar/test_bundle/analysis_summary.html", + "qc/untar/test_bundle/aux_outputs.tar.gz", + "qc/untar/test_bundle/cell_boundaries.csv.gz", + "qc/untar/test_bundle/cell_boundaries.parquet", + "qc/untar/test_bundle/cell_feature_matrix.h5", + "qc/untar/test_bundle/cell_feature_matrix.tar.gz", + "qc/untar/test_bundle/cell_feature_matrix.zarr.zip", + "qc/untar/test_bundle/cells.csv.gz", + "qc/untar/test_bundle/cells.parquet", + "qc/untar/test_bundle/cells.zarr.zip", + "qc/untar/test_bundle/experiment.xenium", + "qc/untar/test_bundle/gene_panel.json", + "qc/untar/test_bundle/metrics_summary.csv", + "qc/untar/test_bundle/morphology.ome.tif", + "qc/untar/test_bundle/morphology_focus", + "qc/untar/test_bundle/morphology_focus/morphology_focus_0000.ome.tif", + "qc/untar/test_bundle/nucleus_boundaries.csv.gz", + "qc/untar/test_bundle/nucleus_boundaries.parquet", + "qc/untar/test_bundle/transcripts.parquet", + "qc/untar/test_bundle/transcripts.zarr.zip", + "spoqc" + ], + [ + ".stub:md5,d41d8cd98f00b204e9800998ecf8427e", + ".stub:md5,d41d8cd98f00b204e9800998ecf8427e", + "fake_file.txt:md5,d41d8cd98f00b204e9800998ecf8427e", + ".end-of-run:md5,d41d8cd98f00b204e9800998ecf8427e", + "analysis.tar.gz:md5,d41d8cd98f00b204e9800998ecf8427e", + "analysis.zarr.zip:md5,d41d8cd98f00b204e9800998ecf8427e", + "analysis_summary.html:md5,d41d8cd98f00b204e9800998ecf8427e", + "aux_outputs.tar.gz:md5,d41d8cd98f00b204e9800998ecf8427e", + "cell_boundaries.csv.gz:md5,d41d8cd98f00b204e9800998ecf8427e", + "cell_boundaries.parquet:md5,d41d8cd98f00b204e9800998ecf8427e", + "cell_feature_matrix.h5:md5,d41d8cd98f00b204e9800998ecf8427e", + "cell_feature_matrix.tar.gz:md5,d41d8cd98f00b204e9800998ecf8427e", + "cell_feature_matrix.zarr.zip:md5,d41d8cd98f00b204e9800998ecf8427e", + "cells.csv.gz:md5,d41d8cd98f00b204e9800998ecf8427e", + "cells.parquet:md5,d41d8cd98f00b204e9800998ecf8427e", + "cells.zarr.zip:md5,d41d8cd98f00b204e9800998ecf8427e", + "experiment.xenium:md5,d41d8cd98f00b204e9800998ecf8427e", + "gene_panel.json:md5,d41d8cd98f00b204e9800998ecf8427e", + "metrics_summary.csv:md5,d41d8cd98f00b204e9800998ecf8427e", + "morphology.ome.tif:md5,d41d8cd98f00b204e9800998ecf8427e", + "morphology_focus_0000.ome.tif:md5,d41d8cd98f00b204e9800998ecf8427e", + "nucleus_boundaries.csv.gz:md5,d41d8cd98f00b204e9800998ecf8427e", + "nucleus_boundaries.parquet:md5,d41d8cd98f00b204e9800998ecf8427e", + "transcripts.parquet:md5,d41d8cd98f00b204e9800998ecf8427e", + "transcripts.zarr.zip:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "timestamp": "2026-08-13T16:15:38.146959204", + "meta": { + "nf-test": "0.9.5", + "nextflow": "26.04.6" + } + } +} \ No newline at end of file diff --git a/workflows/spatialaxe.nf b/workflows/spatialaxe.nf index 3ff05db9..3fafc0a0 100644 --- a/workflows/spatialaxe.nf +++ b/workflows/spatialaxe.nf @@ -17,6 +17,7 @@ include { paramsSummaryMap } from 'plugin/nf-sch // nf-core modules include { UNTAR } from '../modules/nf-core/untar/main' +include { UNZIP } from '../modules/nf-core/unzip/main' // coordinate-based segmentation subworklfows include { SEGGER_CREATE_TRAIN_PREDICT } from '../subworkflows/local/segger_create_train_predict/main' @@ -45,6 +46,7 @@ include { SPATIALDATA_WRITE_META_MERGE } from '../subworkflo // qc layer subworkflows include { OPT_FLIP_TRACK_STAT } from '../subworkflows/local/opt_flip_track_stat/main' +include { SPOQC } from '../subworkflows/local/spoqc/main' /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -94,6 +96,7 @@ workflow SPATIALAXE { stardist_nuclei_model tiling xeniumranger_only + spoqc main: @@ -137,25 +140,46 @@ workflow SPATIALAXE { if (workflow.profile.contains('test')) { // get sample, xenium bundle and image path - ch_input_untar = ch_samplesheet.map { meta, bundle, _image -> + ch_input_compressed = ch_samplesheet.map { meta, bundle, _image, _annotation, _stainings -> return [meta, bundle] } - // get testdata - UNTAR(ch_input_untar) + if (workflow.profile.contains('test_full')) { - ch_untar_outs = UNTAR.out.untar.map { meta, bundle -> - // use toUriString() (not toString()) so the URI scheme (e.g. s3://) - // is preserved when the work dir is on object storage - return [meta, bundle.toUriString()] - } + // get testdata + UNZIP(ch_input_compressed) + + ch_unzip_outs = UNZIP.out.unzipped_archive.map { meta, bundle -> + // use toUriString() (not toString()) so the URI scheme (e.g. s3://) + // is preserved when the work dir is on object storage + return [meta, bundle.toUriString()] + } + + ch_samplesheet + .combine(ch_unzip_outs, by: 0) + .map { meta, _url, image, annotation, stainings, test_bundle -> + return [meta, test_bundle, image, annotation, stainings] + } + .set { ch_input } + + } else { + + // get testdata + UNTAR(ch_input_compressed) - ch_samplesheet - .combine(ch_untar_outs, by: 0) - .map { meta, _url, image, test_bundle -> - return [meta, test_bundle, image] + ch_untar_outs = UNTAR.out.untar.map { meta, bundle -> + // use toUriString() (not toString()) so the URI scheme (e.g. s3://) + // is preserved when the work dir is on object storage + return [meta, bundle.toUriString()] } - .set { ch_input } + + ch_samplesheet + .combine(ch_untar_outs, by: 0) + .map { meta, _url, image, annotation, stainings, test_bundle -> + return [meta, test_bundle, image, annotation, stainings] + } + .set { ch_input } + } } else { // for all other profile runs @@ -164,8 +188,8 @@ workflow SPATIALAXE { if (buffer_samples) { ch_input = ch_samplesheet.buffer(size: buffer_size).map { buffered_sample -> - def (meta, bundle, tif) = buffered_sample[0] - tuple(meta, bundle, tif) + def (meta, bundle, tif, ann, sta) = buffered_sample[0] + tuple(meta, bundle, tif, ann, sta) } } else { @@ -187,7 +211,6 @@ workflow SPATIALAXE { "gene_panel.json", "metrics_summary.csv", "morphology.ome.tif", - "morphology_focus/", "nucleus_boundaries.csv.gz", "nucleus_boundaries.parquet", "transcripts.parquet", @@ -197,10 +220,11 @@ workflow SPATIALAXE { "analysis.tar.gz", "analysis.zarr.zip", "analysis_summary.html", + "morphology_focus/", ] // path to bundle input - ch_bundle_path = ch_input.map { meta, bundle, _image -> + ch_bundle_path = ch_input.map { meta, bundle, _image, _annotation, _stainings -> def bundle_path = file(bundle) if( !bundle_path.exists() ) { @@ -222,7 +246,7 @@ workflow SPATIALAXE { } // get transcript.parquet from the xenium bundle - ch_transcripts_file = ch_input.map { meta, bundle, _image -> + ch_transcripts_file = ch_input.map { meta, bundle, _image, _annotation, _stainings -> def transcripts_parquet = file( file(bundle).toUriString().replaceFirst(/\/$/, '') + "/transcripts.parquet", checkIfExists: true @@ -236,7 +260,7 @@ workflow SPATIALAXE { // v4+: morphology_focus/ch0000_dapi.ome.tif // v1.x: morphology_focus.ome.tif (single file at bundle root) // fallback: morphology.ome.tif (multi-Z stack, not ideal for Cellpose) - ch_morphology_image = ch_input.map { meta, bundle, image -> + ch_morphology_image = ch_input.map { meta, bundle, image, _annotation, _stainings -> def morphology_img if (image) { morphology_img = file(image) @@ -259,7 +283,7 @@ workflow SPATIALAXE { } // get experiment metdata - experiment.xenium - ch_exp_metadata = ch_input.map { meta, bundle, _image -> + ch_exp_metadata = ch_input.map { meta, bundle, _image, _annotation, _stainings -> def exp_metadata = file( file(bundle).toUriString().replaceFirst(/\/$/, '') + "/experiment.xenium", checkIfExists: true @@ -338,14 +362,14 @@ workflow SPATIALAXE { if (gene_panel) { def gene_panel_file = file(gene_panel, checkIfExists: true) - ch_gene_panel = ch_input.map { meta, _bundle, _image -> + ch_gene_panel = ch_input.map { meta, _bundle, _image, _annotation, _stainings -> return [meta, gene_panel_file] } } else { // gene panel to use if only --relabel_genes is provided - ch_gene_panel = ch_input.map { meta, bundle, _image -> + ch_gene_panel = ch_input.map { meta, bundle, _image, _annotation, _stainings -> def gene_panel_file = file( file(bundle).toUriString().replaceFirst(/\/$/, '') + "/gene_panel.json", checkIfExists: true @@ -354,6 +378,19 @@ workflow SPATIALAXE { } } + // get stainings, keyed by meta so per-sample staining lists never mix across samples + ch_stainings = ch_input.flatMap { meta, _bundle, _image, _annotation, stainings -> + def staining_list = (stainings instanceof List) + ? stainings + : stainings.tokenize(';')*.toInteger() + staining_list.collect { staining -> [meta, staining] } + } + + // get annotation + ch_annotation = ch_input.map { meta, _bundle, _image, annotation, _stainings -> + [meta, annotation] + } + /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ SPATIALAXE - RELABEL GENES @@ -589,7 +626,7 @@ workflow SPATIALAXE { */ // run spatialdata modules to generate sd objects in image or coordinate mode - if (mode == 'image' || mode == 'coordinate') { + if (mode == 'image' || mode == 'coordinate' || mode == 'qc') { SPATIALDATA_WRITE_META_MERGE( ch_bundle_path, @@ -598,6 +635,7 @@ workflow SPATIALAXE { cell_segmentation_only, mode, nucleus_segmentation_only, + run_qc, ) } @@ -619,6 +657,16 @@ workflow SPATIALAXE { ch_gene_synonyms, ) } + + + if (spoqc){ + SPOQC ( + SPATIALDATA_WRITE_META_MERGE.out.sd_raw_bundle, + ch_annotation, + ch_stainings, + ) + } + }