diff --git a/modules/nf-core/qcatch/environment.yml b/modules/nf-core/qcatch/environment.yml new file mode 100644 index 00000000000..74aa8df77af --- /dev/null +++ b/modules/nf-core/qcatch/environment.yml @@ -0,0 +1,8 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda + +dependencies: + - bioconda::qcatch=0.2.8 diff --git a/modules/nf-core/qcatch/main.nf b/modules/nf-core/qcatch/main.nf new file mode 100644 index 00000000000..bb39f6fbd1f --- /dev/null +++ b/modules/nf-core/qcatch/main.nf @@ -0,0 +1,58 @@ +process QCATCH { + tag "$meta.id" + label 'process_low' + + conda "${moduleDir}/environment.yml" + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'oras://community.wave.seqera.io/library/qcatch:0.2.8--3089b62e628f96d7': + 'community.wave.seqera.io/library/qcatch:0.2.8--454a9b478b62c36f' }" + input: + tuple val(meta), val(chemistry), path(quant_dir) + + output: + tuple val(meta), path("*.html") , emit: report + tuple val(meta), path("*_filtered_quants.h5ad") , emit: filtered_h5ad + tuple val(meta), path("*_metrics_summary.csv") , emit: metrics_summary + path "versions.yml" , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + def args = task.ext.args ?: '' + def prefix = task.ext.prefix ?: "${meta.id}" + + """ + qcatch \\ + --input ${quant_dir} \\ + --output ${prefix} \\ + --chemistry ${chemistry} \\ + --save_filtered_h5ad \\ + --export_summary_table \\ + ${args} + + mv ${prefix}/QCatch_report.html ${prefix}_qcatch_report.html + mv ${prefix}/filtered_quants.h5ad ${prefix}_filtered_quants.h5ad + mv ${prefix}/summary_table.csv ${prefix}_metrics_summary.csv + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + qcatch: \$(qcatch --version | sed -e "s/qcatch //g") + END_VERSIONS + """ + + stub: + def args = task.ext.args ?: '' + def prefix = task.ext.prefix ?: "${meta.id}" + + """ + touch ${prefix}_qcatch_report.html + touch ${prefix}_filtered_quants.h5ad + touch ${prefix}_metrics_summary.csv + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + qcatch: \$(qcatch --version | sed -e "s/qcatch //g") + END_VERSIONS + """ +} diff --git a/modules/nf-core/qcatch/meta.yml b/modules/nf-core/qcatch/meta.yml new file mode 100644 index 00000000000..1764e1942fb --- /dev/null +++ b/modules/nf-core/qcatch/meta.yml @@ -0,0 +1,86 @@ +name: qcatch +description: Cell-filtering and QC reporting tool for alevin-fry quantification results +keywords: + - single-cell + - quality control + - alevin-fry + - cell filtering + - QC report +tools: + - qcatch: + description: | + QCatch is a quality control and cell filtering tool designed for single-cell RNA-seq data processed by alevin-fry. + It generates comprehensive QC reports and filtered count matrices. + homepage: https://github.com/COMBINE-lab/QCatch + licence: ["BSD-3-Clause"] + identifier: "" +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - chemistry: + type: string + description: | + Chemistry type for the single-cell experiment, which determines the partition range for the empty_drops step. + Supported values: '10X_3p_v2', '10X_3p_v3', '10X_3p_v4', '10X_5p_v3', '10X_3p_LT', '10X_HT'. + If using a standard 10X chemistry and quantification was performed with simpleaf (v0.19.5 or later), + QCatch will try to infer the correct chemistry from the metadata. + - quant_dir: + type: directory + description: | + Directory containing alevin-fry quantification results (af_quant output from simpleaf quant). + Must contain the quantification matrix and associated metadata files. + ontologies: [] +output: + report: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - "*.html": + type: file + description: | + HTML QC report generated by QCatch containing visualizations and metrics + pattern: "*.html" + ontologies: [] + filtered_h5ad: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - "*_filtered_quants.h5ad": + type: file + description: | + Filtered quantification matrix in h5ad format (AnnData) + pattern: "*_filtered_quants.h5ad" + ontologies: [] + metrics_summary: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - "*_metrics_summary.csv": + type: file + description: | + CSV file containing summary metrics from QC analysis + pattern: "*_metrics_summary.csv" + ontologies: [] + versions: + - versions.yml: + type: file + description: | + File containing software versions + pattern: "versions.yml" + ontologies: + - edam: http://edamontology.org/format_3750 # YAML +authors: + - "@wzheng0520" + - "@dongzehe" +maintainers: + - "@wzheng0520" + - "@dongzehe" diff --git a/modules/nf-core/qcatch/tests/main.nf.test b/modules/nf-core/qcatch/tests/main.nf.test new file mode 100644 index 00000000000..c1b99b70bcf --- /dev/null +++ b/modules/nf-core/qcatch/tests/main.nf.test @@ -0,0 +1,70 @@ +nextflow_process { + + name "Test Process QCATCH" + script "../main.nf" + process "QCATCH" + config "./nextflow.config" + + tag "modules" + tag "modules_nfcore" + tag "qcatch" + tag "unzip" + + test("test_qcatch - 1k_pbmc_v3") { + + setup { + // Download and extract QCatch test data from COMBINE-lab using UNZIP module + run("UNZIP") { + script "modules/nf-core/unzip/main.nf" + process { + """ + meta = [id:'qcatch_test_data'] + input[0] = Channel.of([meta, file('https://umd.box.com/shared/static/zd4sai70uw9fs24e1qx6r41ec50pf45g.zip')]) + """ + } + } + } + + when { + process { + """ + // Get the 1k_pbmc_v3 subdirectory from the extracted archive + input[0] = UNZIP.out.unzipped_archive.map { meta, dir -> + def quant_dir = file("\${dir}/test_data/1k_pbmc_v3") + [[id:'1k_pbmc_v3'], '10X_3p_v3', quant_dir] + } + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert file(process.out.report.get(0).get(1)).exists() }, + { assert file(process.out.filtered_h5ad.get(0).get(1)).exists() }, + { assert file(process.out.metrics_summary.get(0).get(1)).exists() }, + { assert snapshot(process.out.versions).match() } + ) + } + } + + test("test_qcatch - stub") { + options "-stub-run" + + when { + process { + """ + meta = [id:'test_stub'] + input[0] = Channel.of([meta, '10X_3p_v3', file('stub_quant_dir')]) + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } +} diff --git a/modules/nf-core/qcatch/tests/main.nf.test.snap b/modules/nf-core/qcatch/tests/main.nf.test.snap new file mode 100644 index 00000000000..d82b2a42341 --- /dev/null +++ b/modules/nf-core/qcatch/tests/main.nf.test.snap @@ -0,0 +1,79 @@ +{ + "test_qcatch - 1k_pbmc_v3": { + "content": [ + [ + "versions.yml:md5,46f73aa6be1b23a13365040ab92e22af" + ] + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.3" + }, + "timestamp": "2026-02-13T21:36:15.677759721" + }, + "test_qcatch - stub": { + "content": [ + { + "0": [ + [ + { + "id": "test_stub" + }, + "test_stub_qcatch_report.html:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "1": [ + [ + { + "id": "test_stub" + }, + "test_stub_filtered_quants.h5ad:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "2": [ + [ + { + "id": "test_stub" + }, + "test_stub_metrics_summary.csv:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "3": [ + "versions.yml:md5,46f73aa6be1b23a13365040ab92e22af" + ], + "filtered_h5ad": [ + [ + { + "id": "test_stub" + }, + "test_stub_filtered_quants.h5ad:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "metrics_summary": [ + [ + { + "id": "test_stub" + }, + "test_stub_metrics_summary.csv:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "report": [ + [ + { + "id": "test_stub" + }, + "test_stub_qcatch_report.html:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "versions": [ + "versions.yml:md5,46f73aa6be1b23a13365040ab92e22af" + ] + } + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.2" + }, + "timestamp": "2026-02-13T21:56:57.673052701" + } +} diff --git a/modules/nf-core/qcatch/tests/nextflow.config b/modules/nf-core/qcatch/tests/nextflow.config new file mode 100644 index 00000000000..edec92b9b9e --- /dev/null +++ b/modules/nf-core/qcatch/tests/nextflow.config @@ -0,0 +1,5 @@ +env { + MPLCONFIGDIR = "./tmp" + NUMBA_CACHE_DIR = "./tmp" + NUMBA_DISABLE_CACHE = 1 +}