From 2cfcc0e57677de8974c8750a4ee1e14ed8685ba3 Mon Sep 17 00:00:00 2001 From: Hwanseok-Jeong Date: Wed, 13 Aug 2025 16:25:08 +0200 Subject: [PATCH 01/33] fgbio module added --- modules.json | 45 ++++ .../callduplexconsensusreads/environment.yml | 7 + .../fgbio/callduplexconsensusreads/main.nf | 70 ++++++ .../fgbio/callduplexconsensusreads/meta.yml | 57 +++++ .../tests/main.nf.test | 62 +++++ .../tests/main.nf.test.snap | 72 ++++++ .../environment.yml | 7 + .../fgbio/callmolecularconsensusreads/main.nf | 64 +++++ .../callmolecularconsensusreads/meta.yml | 55 +++++ .../tests/main.nf.test | 72 ++++++ .../tests/main.nf.test.snap | 68 ++++++ .../tests/sort.config | 6 + .../collectduplexseqmetrics/environment.yml | 8 + .../fgbio/collectduplexseqmetrics/main.nf | 80 +++++++ .../fgbio/collectduplexseqmetrics/meta.yml | 130 +++++++++++ .../tests/main.nf.test | 79 +++++++ .../tests/main.nf.test.snap | 106 +++++++++ .../fgbio/copyumifromreadname/environment.yml | 7 + .../nf-core/fgbio/copyumifromreadname/main.nf | 64 +++++ .../fgbio/copyumifromreadname/meta.yml | 80 +++++++ .../copyumifromreadname/tests/main.nf.test | 75 ++++++ .../tests/main.nf.test.snap | 110 +++++++++ .../copyumifromreadname/tests/nextflow.config | 5 + .../nf-core/fgbio/fastqtobam/environment.yml | 7 + modules/nf-core/fgbio/fastqtobam/main.nf | 70 ++++++ modules/nf-core/fgbio/fastqtobam/meta.yml | 65 ++++++ .../nf-core/fgbio/fastqtobam/tests/bam.config | 3 + .../fgbio/fastqtobam/tests/cram.config | 3 + .../fastqtobam/tests/custom_sample.config | 3 + .../fgbio/fastqtobam/tests/main.nf.test | 218 ++++++++++++++++++ .../fgbio/fastqtobam/tests/main.nf.test.snap | 114 +++++++++ .../nf-core/fgbio/fastqtobam/tests/umi.config | 3 + .../filterconsensusreads/environment.yml | 7 + .../fgbio/filterconsensusreads/main.nf | 71 ++++++ .../fgbio/filterconsensusreads/meta.yml | 74 ++++++ .../filterconsensusreads/tests/main.nf.test | 69 ++++++ .../tests/main.nf.test.snap | 72 ++++++ .../fgbio/groupreadsbyumi/environment.yml | 7 + modules/nf-core/fgbio/groupreadsbyumi/main.nf | 70 ++++++ .../nf-core/fgbio/groupreadsbyumi/meta.yml | 84 +++++++ .../fgbio/groupreadsbyumi/tests/main.nf.test | 60 +++++ .../groupreadsbyumi/tests/main.nf.test.snap | 144 ++++++++++++ modules/nf-core/fgbio/sortbam/environment.yml | 8 + modules/nf-core/fgbio/sortbam/main.nf | 61 +++++ modules/nf-core/fgbio/sortbam/meta.yml | 50 ++++ .../nf-core/fgbio/sortbam/tests/main.nf.test | 56 +++++ .../fgbio/sortbam/tests/main.nf.test.snap | 68 ++++++ .../nf-core/fgbio/zipperbams/environment.yml | 7 + modules/nf-core/fgbio/zipperbams/main.nf | 73 ++++++ modules/nf-core/fgbio/zipperbams/meta.yml | 82 +++++++ .../fgbio/zipperbams/tests/main.nf.test | 83 +++++++ .../fgbio/zipperbams/tests/main.nf.test.snap | 72 ++++++ .../fgbio/zipperbams/tests/nextflow.config | 5 + 53 files changed, 3038 insertions(+) create mode 100644 modules/nf-core/fgbio/callduplexconsensusreads/environment.yml create mode 100644 modules/nf-core/fgbio/callduplexconsensusreads/main.nf create mode 100644 modules/nf-core/fgbio/callduplexconsensusreads/meta.yml create mode 100644 modules/nf-core/fgbio/callduplexconsensusreads/tests/main.nf.test create mode 100644 modules/nf-core/fgbio/callduplexconsensusreads/tests/main.nf.test.snap create mode 100644 modules/nf-core/fgbio/callmolecularconsensusreads/environment.yml create mode 100644 modules/nf-core/fgbio/callmolecularconsensusreads/main.nf create mode 100644 modules/nf-core/fgbio/callmolecularconsensusreads/meta.yml create mode 100644 modules/nf-core/fgbio/callmolecularconsensusreads/tests/main.nf.test create mode 100644 modules/nf-core/fgbio/callmolecularconsensusreads/tests/main.nf.test.snap create mode 100644 modules/nf-core/fgbio/callmolecularconsensusreads/tests/sort.config create mode 100644 modules/nf-core/fgbio/collectduplexseqmetrics/environment.yml create mode 100644 modules/nf-core/fgbio/collectduplexseqmetrics/main.nf create mode 100644 modules/nf-core/fgbio/collectduplexseqmetrics/meta.yml create mode 100644 modules/nf-core/fgbio/collectduplexseqmetrics/tests/main.nf.test create mode 100644 modules/nf-core/fgbio/collectduplexseqmetrics/tests/main.nf.test.snap create mode 100644 modules/nf-core/fgbio/copyumifromreadname/environment.yml create mode 100644 modules/nf-core/fgbio/copyumifromreadname/main.nf create mode 100644 modules/nf-core/fgbio/copyumifromreadname/meta.yml create mode 100644 modules/nf-core/fgbio/copyumifromreadname/tests/main.nf.test create mode 100644 modules/nf-core/fgbio/copyumifromreadname/tests/main.nf.test.snap create mode 100644 modules/nf-core/fgbio/copyumifromreadname/tests/nextflow.config create mode 100644 modules/nf-core/fgbio/fastqtobam/environment.yml create mode 100644 modules/nf-core/fgbio/fastqtobam/main.nf create mode 100644 modules/nf-core/fgbio/fastqtobam/meta.yml create mode 100644 modules/nf-core/fgbio/fastqtobam/tests/bam.config create mode 100644 modules/nf-core/fgbio/fastqtobam/tests/cram.config create mode 100644 modules/nf-core/fgbio/fastqtobam/tests/custom_sample.config create mode 100644 modules/nf-core/fgbio/fastqtobam/tests/main.nf.test create mode 100644 modules/nf-core/fgbio/fastqtobam/tests/main.nf.test.snap create mode 100644 modules/nf-core/fgbio/fastqtobam/tests/umi.config create mode 100644 modules/nf-core/fgbio/filterconsensusreads/environment.yml create mode 100644 modules/nf-core/fgbio/filterconsensusreads/main.nf create mode 100644 modules/nf-core/fgbio/filterconsensusreads/meta.yml create mode 100644 modules/nf-core/fgbio/filterconsensusreads/tests/main.nf.test create mode 100644 modules/nf-core/fgbio/filterconsensusreads/tests/main.nf.test.snap create mode 100644 modules/nf-core/fgbio/groupreadsbyumi/environment.yml create mode 100644 modules/nf-core/fgbio/groupreadsbyumi/main.nf create mode 100644 modules/nf-core/fgbio/groupreadsbyumi/meta.yml create mode 100644 modules/nf-core/fgbio/groupreadsbyumi/tests/main.nf.test create mode 100644 modules/nf-core/fgbio/groupreadsbyumi/tests/main.nf.test.snap create mode 100644 modules/nf-core/fgbio/sortbam/environment.yml create mode 100644 modules/nf-core/fgbio/sortbam/main.nf create mode 100644 modules/nf-core/fgbio/sortbam/meta.yml create mode 100644 modules/nf-core/fgbio/sortbam/tests/main.nf.test create mode 100644 modules/nf-core/fgbio/sortbam/tests/main.nf.test.snap create mode 100644 modules/nf-core/fgbio/zipperbams/environment.yml create mode 100644 modules/nf-core/fgbio/zipperbams/main.nf create mode 100644 modules/nf-core/fgbio/zipperbams/meta.yml create mode 100644 modules/nf-core/fgbio/zipperbams/tests/main.nf.test create mode 100644 modules/nf-core/fgbio/zipperbams/tests/main.nf.test.snap create mode 100644 modules/nf-core/fgbio/zipperbams/tests/nextflow.config diff --git a/modules.json b/modules.json index d860bfc6..fc90200c 100644 --- a/modules.json +++ b/modules.json @@ -51,6 +51,51 @@ "installed_by": ["modules"], "patch": "modules/nf-core/fastp/fastp.diff" }, + "fgbio/callduplexconsensusreads": { + "branch": "master", + "git_sha": "41dfa3f7c0ffabb96a6a813fe321c6d1cc5b6e46", + "installed_by": ["modules"] + }, + "fgbio/callmolecularconsensusreads": { + "branch": "master", + "git_sha": "41dfa3f7c0ffabb96a6a813fe321c6d1cc5b6e46", + "installed_by": ["modules"] + }, + "fgbio/collectduplexseqmetrics": { + "branch": "master", + "git_sha": "41dfa3f7c0ffabb96a6a813fe321c6d1cc5b6e46", + "installed_by": ["modules"] + }, + "fgbio/copyumifromreadname": { + "branch": "master", + "git_sha": "47dbfc0fbcd8e4e3b73d843f4659069441ca8692", + "installed_by": ["modules"] + }, + "fgbio/fastqtobam": { + "branch": "master", + "git_sha": "41dfa3f7c0ffabb96a6a813fe321c6d1cc5b6e46", + "installed_by": ["modules"] + }, + "fgbio/filterconsensusreads": { + "branch": "master", + "git_sha": "41dfa3f7c0ffabb96a6a813fe321c6d1cc5b6e46", + "installed_by": ["modules"] + }, + "fgbio/groupreadsbyumi": { + "branch": "master", + "git_sha": "41dfa3f7c0ffabb96a6a813fe321c6d1cc5b6e46", + "installed_by": ["modules"] + }, + "fgbio/sortbam": { + "branch": "master", + "git_sha": "41dfa3f7c0ffabb96a6a813fe321c6d1cc5b6e46", + "installed_by": ["modules"] + }, + "fgbio/zipperbams": { + "branch": "master", + "git_sha": "41dfa3f7c0ffabb96a6a813fe321c6d1cc5b6e46", + "installed_by": ["modules"] + }, "md5sum": { "branch": "master", "git_sha": "666652151335353eef2fcd58880bcef5bc2928e1", diff --git a/modules/nf-core/fgbio/callduplexconsensusreads/environment.yml b/modules/nf-core/fgbio/callduplexconsensusreads/environment.yml new file mode 100644 index 00000000..4dbb6856 --- /dev/null +++ b/modules/nf-core/fgbio/callduplexconsensusreads/environment.yml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - bioconda::fgbio=2.5.21 diff --git a/modules/nf-core/fgbio/callduplexconsensusreads/main.nf b/modules/nf-core/fgbio/callduplexconsensusreads/main.nf new file mode 100644 index 00000000..be6fc97a --- /dev/null +++ b/modules/nf-core/fgbio/callduplexconsensusreads/main.nf @@ -0,0 +1,70 @@ +process FGBIO_CALLDUPLEXCONSENSUSREADS { + tag "$meta.id" + label 'process_single' + + conda "${moduleDir}/environment.yml" + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/b4/b4047e3e517b57fae311eab139a12f0887d898b7da5fceeb2a1029c73b9e3904/data' : + 'community.wave.seqera.io/library/fgbio:2.5.21--368dab1b4f308243' }" + + input: + tuple val(meta), path(grouped_bam) + val min_reads + val min_baseq + + output: + tuple val(meta), path("${prefix}.bam"), emit: bam + path "versions.yml" , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + def args = task.ext.args ?: '' + prefix = task.ext.prefix ?: "${meta.id}_consensus_unmapped" + + def mem_gb = 8 + if (!task.memory) { + log.info '[fgbio CallDuplexConsensusReads] Available memory not known - defaulting to 8GB. Specify process memory requirements to change this.' + } else if (mem_gb > task.memory.giga) { + if (task.memory.giga < 2) { + mem_gb = 1 + } else { + mem_gb = task.memory.giga - 1 + } + } + if ("$grouped_bam" == "${prefix}.bam") error "Input and output names are the same, use \"task.ext.prefix\" to disambiguate!" + + """ + fgbio \\ + -Xmx${mem_gb}g \\ + --tmp-dir=. \\ + --async-io=true \\ + --compression=1 \\ + CallDuplexConsensusReads \\ + --input $grouped_bam \\ + --output ${prefix}.bam \\ + --min-reads ${min_reads} \\ + --min-input-base-quality ${min_baseq} \\ + --threads ${task.cpus} \\ + $args + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + fgbio: \$( echo \$(fgbio --version 2>&1 | tr -d '[:cntrl:]' ) | sed -e 's/^.*Version: //;s/\\[.*\$//') + END_VERSIONS + """ + + stub: + prefix = task.ext.prefix ?: "${meta.id}_consensus_unmapped" + if ("$grouped_bam" == "${prefix}.bam") error "Input and output names are the same, use \"task.ext.prefix\" to disambiguate!" + """ + touch ${prefix}.bam + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + fgbio: \$( echo \$(fgbio --version 2>&1 | tr -d '[:cntrl:]' ) | sed -e 's/^.*Version: //;s/\\[.*\$//') + END_VERSIONS + """ + +} diff --git a/modules/nf-core/fgbio/callduplexconsensusreads/meta.yml b/modules/nf-core/fgbio/callduplexconsensusreads/meta.yml new file mode 100644 index 00000000..3b615eda --- /dev/null +++ b/modules/nf-core/fgbio/callduplexconsensusreads/meta.yml @@ -0,0 +1,57 @@ +name: "fgbio_callduplexconsensusreads" +description: Uses FGBIO CallDuplexConsensusReads to call duplex consensus sequences + from reads generated from the same double-stranded source molecule. +keywords: + - umi + - duplex + - fgbio +tools: + - "fgbio": + description: "A set of tools for working with genomic and high throughput sequencing + data, including UMIs" + homepage: http://fulcrumgenomics.github.io/fgbio/ + documentation: http://fulcrumgenomics.github.io/fgbio/tools/latest/CallDuplexConsensusReads.html + tool_dev_url: https://github.com/fulcrumgenomics/fgbio + licence: ["MIT"] + identifier: biotools:fgbio +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - grouped_bam: + type: file + description: Grouped BAM file + pattern: "*.bam" + ontologies: [] + - min_reads: + type: string + description: Minimum number of raw/original reads to build each consensus read. Can + be a space delimited list of 1-3 values. See fgbio documentation for more details. + - min_baseq: + type: integer + description: Ignore bases in raw reads that have Q below this value +output: + bam: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - ${prefix}.bam: + type: file + description: consensus BAM file + pattern: "*.bam" + ontologies: [] + versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" + ontologies: + - edam: http://edamontology.org/format_3750 # YAML +authors: + - "@lescai" +maintainers: + - "@lescai" diff --git a/modules/nf-core/fgbio/callduplexconsensusreads/tests/main.nf.test b/modules/nf-core/fgbio/callduplexconsensusreads/tests/main.nf.test new file mode 100644 index 00000000..0144e0ea --- /dev/null +++ b/modules/nf-core/fgbio/callduplexconsensusreads/tests/main.nf.test @@ -0,0 +1,62 @@ +nextflow_process { + + name "Test Process FGBIO_CALLDUPLEXCONSENSUSREADS" + script "../main.nf" + process "FGBIO_CALLDUPLEXCONSENSUSREADS" + + tag "modules" + tag "modules_nfcore" + tag "fgbio" + tag "fgbio/callduplexconsensusreads" + + test("homo_sapiens - bam") { + + when { + process { + """ + input[0] = [ + [ id:'test', single_end:false ], // meta map + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/bam/umi/test.paired_end.duplex_umi_grouped.bam', checkIfExists: true) + ] + input[1] = 3 + input[2] = 20 + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + + test("homo_sapiens - stub") { + + options "-stub" + + when { + process { + """ + input[0] = [ + [ id:'test', single_end:false ], // meta map + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/bam/umi/test.paired_end.duplex_umi_grouped.bam', checkIfExists: true) + ] + input[1] = 3 + input[2] = 20 + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + +} diff --git a/modules/nf-core/fgbio/callduplexconsensusreads/tests/main.nf.test.snap b/modules/nf-core/fgbio/callduplexconsensusreads/tests/main.nf.test.snap new file mode 100644 index 00000000..dcc7f9c8 --- /dev/null +++ b/modules/nf-core/fgbio/callduplexconsensusreads/tests/main.nf.test.snap @@ -0,0 +1,72 @@ +{ + "homo_sapiens - stub": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test_consensus_unmapped.bam:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "1": [ + "versions.yml:md5,337eebefcdf12475174a668e31bb4245" + ], + "bam": [ + [ + { + "id": "test", + "single_end": false + }, + "test_consensus_unmapped.bam:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "versions": [ + "versions.yml:md5,337eebefcdf12475174a668e31bb4245" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.2" + }, + "timestamp": "2025-06-06T16:32:47.930923" + }, + "homo_sapiens - bam": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test_consensus_unmapped.bam:md5,4f0e87feb7601d06617c9f29d7aec352" + ] + ], + "1": [ + "versions.yml:md5,337eebefcdf12475174a668e31bb4245" + ], + "bam": [ + [ + { + "id": "test", + "single_end": false + }, + "test_consensus_unmapped.bam:md5,4f0e87feb7601d06617c9f29d7aec352" + ] + ], + "versions": [ + "versions.yml:md5,337eebefcdf12475174a668e31bb4245" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.2" + }, + "timestamp": "2025-06-08T13:04:50.447095" + } +} \ No newline at end of file diff --git a/modules/nf-core/fgbio/callmolecularconsensusreads/environment.yml b/modules/nf-core/fgbio/callmolecularconsensusreads/environment.yml new file mode 100644 index 00000000..4dbb6856 --- /dev/null +++ b/modules/nf-core/fgbio/callmolecularconsensusreads/environment.yml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - bioconda::fgbio=2.5.21 diff --git a/modules/nf-core/fgbio/callmolecularconsensusreads/main.nf b/modules/nf-core/fgbio/callmolecularconsensusreads/main.nf new file mode 100644 index 00000000..e1d869b3 --- /dev/null +++ b/modules/nf-core/fgbio/callmolecularconsensusreads/main.nf @@ -0,0 +1,64 @@ +process FGBIO_CALLMOLECULARCONSENSUSREADS { + tag "$meta.id" + label 'process_medium' + + conda "${moduleDir}/environment.yml" + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/b4/b4047e3e517b57fae311eab139a12f0887d898b7da5fceeb2a1029c73b9e3904/data' : + 'community.wave.seqera.io/library/fgbio:2.5.21--368dab1b4f308243' }" + + input: + tuple val(meta), path(grouped_bam) + val min_reads + val min_baseq + + output: + tuple val(meta), path("*.bam"), emit: bam + 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}_consensus_unmapped" + def mem_gb = 8 + if (!task.memory) { + log.info '[fgbio CallMolecularConsensusReads] Available memory not known - defaulting to 8GB. Specify process memory requirements to change this.' + } else { + mem_gb = task.memory.giga + } + if ("$grouped_bam" == "${prefix}.bam") error "Input and output names are the same, use \"task.ext.prefix\" to disambiguate!" + """ + fgbio \\ + -Xmx${mem_gb}g \\ + --tmp-dir=. \\ + --async-io=true \\ + --compression=1 \\ + CallMolecularConsensusReads \\ + --input $grouped_bam \\ + --output ${prefix}.bam \\ + --min-reads ${min_reads} \\ + --min-input-base-quality ${min_baseq} \\ + --threads ${task.cpus} \\ + $args; + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + fgbio: \$( echo \$(fgbio --version 2>&1 | tr -d '[:cntrl:]' ) | sed -e 's/^.*Version: //;s/\\[.*\$//') + END_VERSIONS + """ + + stub: + prefix = task.ext.prefix ?: "${meta.id}_consensus_unmapped" + if ("$grouped_bam" == "${prefix}.bam") error "Input and output names are the same, use \"task.ext.prefix\" to disambiguate!" + """ + touch ${prefix}.bam + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + fgbio: \$( echo \$(fgbio --version 2>&1 | tr -d '[:cntrl:]' ) | sed -e 's/^.*Version: //;s/\\[.*\$//') + END_VERSIONS + """ + +} diff --git a/modules/nf-core/fgbio/callmolecularconsensusreads/meta.yml b/modules/nf-core/fgbio/callmolecularconsensusreads/meta.yml new file mode 100644 index 00000000..c7b75eb7 --- /dev/null +++ b/modules/nf-core/fgbio/callmolecularconsensusreads/meta.yml @@ -0,0 +1,55 @@ +name: fgbio_callmolecularconsensusreads +description: Calls consensus sequences from reads with the same unique molecular tag. +keywords: + - UMIs + - consensus sequence + - bam +tools: + - fgbio: + description: Tools for working with genomic and high throughput sequencing data. + homepage: https://github.com/fulcrumgenomics/fgbio + documentation: http://fulcrumgenomics.github.io/fgbio/ + licence: ["MIT"] + identifier: biotools:fgbio +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false, collapse:false ] + - grouped_bam: + type: file + description: | + The input SAM or BAM file, grouped by UMIs + pattern: "*.{bam,sam}" + ontologies: [] + - min_reads: + type: integer + description: Minimum number of original reads to build each consensus read. + - min_baseq: + type: integer + description: Ignore bases in raw reads that have Q below this value. +output: + bam: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - "*.bam": + type: file + description: | + Output SAM or BAM file to write consensus reads. + pattern: "*.{bam,sam}" + ontologies: [] + versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" + ontologies: + - edam: http://edamontology.org/format_3750 # YAML +authors: + - "@sruthipsuresh" +maintainers: + - "@sruthipsuresh" diff --git a/modules/nf-core/fgbio/callmolecularconsensusreads/tests/main.nf.test b/modules/nf-core/fgbio/callmolecularconsensusreads/tests/main.nf.test new file mode 100644 index 00000000..8a906340 --- /dev/null +++ b/modules/nf-core/fgbio/callmolecularconsensusreads/tests/main.nf.test @@ -0,0 +1,72 @@ +nextflow_process { + + name "Test Process FGBIO_CALLMOLECULARCONSENSUSREADS" + script "../main.nf" + process "FGBIO_CALLMOLECULARCONSENSUSREADS" + + tag "modules" + tag "modules_nfcore" + tag "fgbio" + tag "fgbio/callmolecularconsensusreads" + tag "fgbio/sortbam" + + setup { + + run("FGBIO_SORTBAM") { + script "../../sortbam/main.nf" + config "./sort.config" + process { + """ + input[0] = [[ id:'homo_sapiens_genome' ], + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/bam/test.single_end.bam', checkIfExists: true) + ] + """ + } + } + } + + test("homo_sapiens - bam") { + + when { + process { + """ + input[0] = FGBIO_SORTBAM.out.bam + input[1] = 1 + input[2] = 20 + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + + test("homo_sapiens - stub") { + + options "-stub" + + when { + process { + """ + input[0] = FGBIO_SORTBAM.out.bam + input[1] = 1 + input[2] = 20 + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + +} diff --git a/modules/nf-core/fgbio/callmolecularconsensusreads/tests/main.nf.test.snap b/modules/nf-core/fgbio/callmolecularconsensusreads/tests/main.nf.test.snap new file mode 100644 index 00000000..f37f1bd7 --- /dev/null +++ b/modules/nf-core/fgbio/callmolecularconsensusreads/tests/main.nf.test.snap @@ -0,0 +1,68 @@ +{ + "homo_sapiens - stub": { + "content": [ + { + "0": [ + [ + { + "id": "homo_sapiens_genome" + }, + "homo_sapiens_genome_consensus_unmapped.bam:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "1": [ + "versions.yml:md5,8dbdae0c815fd6be2c3090ca83f6bbc6" + ], + "bam": [ + [ + { + "id": "homo_sapiens_genome" + }, + "homo_sapiens_genome_consensus_unmapped.bam:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "versions": [ + "versions.yml:md5,8dbdae0c815fd6be2c3090ca83f6bbc6" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.2" + }, + "timestamp": "2025-06-06T16:33:48.560245" + }, + "homo_sapiens - bam": { + "content": [ + { + "0": [ + [ + { + "id": "homo_sapiens_genome" + }, + "homo_sapiens_genome_consensus_unmapped.bam:md5,f56c861f1f604ecc9894dc9182b170f8" + ] + ], + "1": [ + "versions.yml:md5,8dbdae0c815fd6be2c3090ca83f6bbc6" + ], + "bam": [ + [ + { + "id": "homo_sapiens_genome" + }, + "homo_sapiens_genome_consensus_unmapped.bam:md5,f56c861f1f604ecc9894dc9182b170f8" + ] + ], + "versions": [ + "versions.yml:md5,8dbdae0c815fd6be2c3090ca83f6bbc6" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.2" + }, + "timestamp": "2025-06-08T13:05:45.874565" + } +} \ No newline at end of file diff --git a/modules/nf-core/fgbio/callmolecularconsensusreads/tests/sort.config b/modules/nf-core/fgbio/callmolecularconsensusreads/tests/sort.config new file mode 100644 index 00000000..b205c8f2 --- /dev/null +++ b/modules/nf-core/fgbio/callmolecularconsensusreads/tests/sort.config @@ -0,0 +1,6 @@ +process { + withName: FGBIO_SORTBAM { + ext.args = '-s TemplateCoordinate' + ext.prefix = { "${meta.id}_out" } + } +} diff --git a/modules/nf-core/fgbio/collectduplexseqmetrics/environment.yml b/modules/nf-core/fgbio/collectduplexseqmetrics/environment.yml new file mode 100644 index 00000000..f83d1262 --- /dev/null +++ b/modules/nf-core/fgbio/collectduplexseqmetrics/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::fgbio=2.5.21 + - conda-forge::r-ggplot2=3.5.2 diff --git a/modules/nf-core/fgbio/collectduplexseqmetrics/main.nf b/modules/nf-core/fgbio/collectduplexseqmetrics/main.nf new file mode 100644 index 00000000..9edf0ee8 --- /dev/null +++ b/modules/nf-core/fgbio/collectduplexseqmetrics/main.nf @@ -0,0 +1,80 @@ +process FGBIO_COLLECTDUPLEXSEQMETRICS { + tag "$meta.id" + label 'process_single' + + conda "${moduleDir}/environment.yml" + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/b4/b4047e3e517b57fae311eab139a12f0887d898b7da5fceeb2a1029c73b9e3904/data' : + 'community.wave.seqera.io/library/fgbio:2.5.21--368dab1b4f308243' }" + + input: + tuple val(meta), path(grouped_bam) + path interval_list + + output: + tuple val(meta), path("**.family_sizes.txt") , emit: family_sizes + tuple val(meta), path("**.duplex_family_sizes.txt") , emit: duplex_family_sizes + tuple val(meta), path("**.duplex_yield_metrics.txt"), emit: duplex_yield_metrics + tuple val(meta), path("**.umi_counts.txt") , emit: umi_counts + tuple val(meta), path("**.duplex_qc.pdf") , emit: duplex_qc + tuple val(meta), path("**.duplex_umi_counts.txt") , emit: duplex_umi_counts, optional: true + 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}" + def intervals = interval_list ? "--intervals ${interval_list}" : "" + def mem_gb = 8 + + if (!task.memory) { + log.info '[fgbio CollectDuplexSeqMetrics] Available memory not known - defaulting to 8GB. Specify process memory requirements to change this.' + } else if (mem_gb > task.memory.giga) { + if (task.memory.giga < 2) { + mem_gb = 1 + } else { + mem_gb = task.memory.giga - 1 + } + } + + """ + fgbio \\ + -Xmx${mem_gb}g \\ + --tmp-dir=. \\ + --async-io=true \\ + --compression=1 \\ + CollectDuplexSeqMetrics \\ + --input $grouped_bam \\ + --output ${prefix} \\ + $intervals \\ + $args + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + fgbio: \$( echo \$(fgbio --version 2>&1 | tr -d '[:cntrl:]' ) | sed -e 's/^.*Version: //;s/\\[.*\$//') + ggplot2: \$(Rscript -e "library(ggplot2); cat(as.character(packageVersion('ggplot2')))") + END_VERSIONS + """ + + stub: + def args = task.ext.args ?: '' + def prefix = task.ext.prefix ?: "${meta.id}" + def touch_duplex_umi = args.contains("--duplex-umi-counts") || args.contains("-u") ? "touch ${prefix}.duplex_umi_counts.txt" : "" + + """ + touch ${prefix}.family_sizes.txt + touch ${prefix}.duplex_family_sizes.txt + touch ${prefix}.duplex_yield_metrics.txt + touch ${prefix}.umi_counts.txt + touch ${prefix}.duplex_qc.pdf + $touch_duplex_umi + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + fgbio: \$( echo \$(fgbio --version 2>&1 | tr -d '[:cntrl:]' ) | sed -e 's/^.*Version: //;s/\\[.*\$//') + ggplot2: \$(Rscript -e "library(ggplot2); cat(as.character(packageVersion('ggplot2')))") + END_VERSIONS + """ +} diff --git a/modules/nf-core/fgbio/collectduplexseqmetrics/meta.yml b/modules/nf-core/fgbio/collectduplexseqmetrics/meta.yml new file mode 100644 index 00000000..947540b3 --- /dev/null +++ b/modules/nf-core/fgbio/collectduplexseqmetrics/meta.yml @@ -0,0 +1,130 @@ +name: "fgbio_collectduplexseqmetrics" +description: Collects a suite of metrics to QC duplex sequencing data. +keywords: + - UMIs + - QC + - bam + - duplex +tools: + - "fgbio": + description: "A set of tools for working with genomic and high throughput sequencing + data, including UMIs" + homepage: "http://fulcrumgenomics.github.io/fgbio/" + documentation: "http://fulcrumgenomics.github.io/fgbio/" + tool_dev_url: "https://github.com/fulcrumgenomics/fgbio" + licence: ["MIT"] + identifier: biotools:fgbio + - "r-ggplot2": + description: "ggplot2 is a system for declaratively creating graphics, based on + The Grammar of Graphics. " + homepage: "https://ggplot2.tidyverse.org/" + documentation: "https://ggplot2.tidyverse.org/" + tool_dev_url: "https://github.com/tidyverse/ggplot2" + licence: ["MIT"] + identifier: biotools:fgbio + +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'sample1' ]` + - grouped_bam: + type: file + description: It has to be either 1)The exact BAM output by the GroupReadsByUmi + tool (in the sort-order it was produced in) 2)A BAM file that has MI tags + present on all reads (usually set by GroupReadsByUmi and has been sorted with + SortBam into TemplateCoordinate order. + pattern: "*.bam" + ontologies: [] + - interval_list: + type: file + description: Calculation of metrics may be restricted to a set of regions using + the --intervals parameter. The file format is descripted here + https://samtools.github.io/htsjdk/javadoc/htsjdk/index.html?htsjdk/samtools/util/Interval.html + pattern: "*.{tsv|txt|interval_list}" + ontologies: [] +output: + family_sizes: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'sample1' ]` + - "**.family_sizes.txt": + type: file + description: Metrics on the frequency of different types of families of different + sizes + pattern: "*.txt" + ontologies: [] + duplex_family_sizes: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'sample1' ]` + - "**.duplex_family_sizes.txt": + type: file + description: Metrics on the frequency of duplex tag families by the number + of observations from each strand + pattern: "*.txt" + ontologies: [] + duplex_yield_metrics: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'sample1' ]` + - "**.duplex_yield_metrics.txt": + type: file + description: Summary QC metrics produced using 5%, 10%, 15%...100% of the + data + pattern: "*.txt" + ontologies: [] + umi_counts: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'sample1' ]` + - "**.umi_counts.txt": + type: file + description: Metrics on the frequency of observations of UMIs within reads + and tag families + pattern: "*.txt" + ontologies: [] + duplex_qc: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'sample1' ]` + - "**.duplex_qc.pdf": + type: file + description: A series of plots generated from the preceding metrics files + for visualization + pattern: "*.pdf" + ontologies: [] + duplex_umi_counts: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'sample1' ]` + - "**.duplex_umi_counts.txt": + type: file + description: Metrics on the frequency of observations of duplex UMIs within + reads and tag families. + pattern: "*.txt" + ontologies: [] + versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" + ontologies: + - edam: http://edamontology.org/format_3750 # YAML +authors: + - "@georgiakes" +maintainers: + - "@georgiakes" diff --git a/modules/nf-core/fgbio/collectduplexseqmetrics/tests/main.nf.test b/modules/nf-core/fgbio/collectduplexseqmetrics/tests/main.nf.test new file mode 100644 index 00000000..0021229b --- /dev/null +++ b/modules/nf-core/fgbio/collectduplexseqmetrics/tests/main.nf.test @@ -0,0 +1,79 @@ +nextflow_process { + + name "Test Process FGBIO_COLLECTDUPLEXSEQMETRICS" + script "../main.nf" + process "FGBIO_COLLECTDUPLEXSEQMETRICS" + + tag "modules" + tag "modules_nfcore" + tag "fgbio" + tag "fgbio/collectduplexseqmetrics" + + + test("homo_sapiens - bam") { + + when { + process { + """ + input[0] = [ + [ id:'test', single_end:false ], // meta map + file(params.modules_testdata_base_path + '/genomics/homo_sapiens/illumina/bam/umi/test.paired_end.duplex_umi_grouped.bam', checkIfExists: true) + ] + input[1]=[] + + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out.family_sizes, + process.out.duplex_family_sizes, + process.out.duplex_yield_metrics, + process.out.umi_counts, + process.out.duplex_umi_counts, + process.out.versions, + file(process.out.duplex_qc[0][1]).name) + .match() } + + ) + } + + } + + test("homo_sapiens - stub") { + + options "-stub" + + when { + process { + """ + input[0] = [ + [ id:'test', single_end:false ], // meta map + file(params.modules_testdata_base_path + '/genomics/homo_sapiens/illumina/bam/umi/test.paired_end.duplex_umi_grouped.bam', checkIfExists: true) + ] + input[1] = [] + + + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out.family_sizes, + process.out.duplex_family_sizes, + process.out.duplex_yield_metrics, + process.out.umi_counts, + process.out.duplex_umi_counts, + process.out.versions, + file(process.out.duplex_qc[0][1]).name) + .match() } + ) + } + + } + +} \ No newline at end of file diff --git a/modules/nf-core/fgbio/collectduplexseqmetrics/tests/main.nf.test.snap b/modules/nf-core/fgbio/collectduplexseqmetrics/tests/main.nf.test.snap new file mode 100644 index 00000000..f7b9547f --- /dev/null +++ b/modules/nf-core/fgbio/collectduplexseqmetrics/tests/main.nf.test.snap @@ -0,0 +1,106 @@ +{ + "homo_sapiens - stub": { + "content": [ + [ + [ + { + "id": "test", + "single_end": false + }, + "test.family_sizes.txt:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + [ + [ + { + "id": "test", + "single_end": false + }, + "test.duplex_family_sizes.txt:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + [ + [ + { + "id": "test", + "single_end": false + }, + "test.duplex_yield_metrics.txt:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + [ + [ + { + "id": "test", + "single_end": false + }, + "test.umi_counts.txt:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + [ + + ], + [ + "versions.yml:md5,d8d6be2d6162514abe0b38fa29f963c4" + ], + "test.duplex_qc.pdf" + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.2" + }, + "timestamp": "2025-06-08T13:07:01.106818" + }, + "homo_sapiens - bam": { + "content": [ + [ + [ + { + "id": "test", + "single_end": false + }, + "test.family_sizes.txt:md5,a49de49bd587440c316fec830f502620" + ] + ], + [ + [ + { + "id": "test", + "single_end": false + }, + "test.duplex_family_sizes.txt:md5,129e41170b9f5f2f8edce62a686c8548" + ] + ], + [ + [ + { + "id": "test", + "single_end": false + }, + "test.duplex_yield_metrics.txt:md5,237e4e4ee713fdf672b0ee796827fb9d" + ] + ], + [ + [ + { + "id": "test", + "single_end": false + }, + "test.umi_counts.txt:md5,9fe38b2a49ca80492b3a1c6a55679155" + ] + ], + [ + + ], + [ + "versions.yml:md5,d8d6be2d6162514abe0b38fa29f963c4" + ], + "test.duplex_qc.pdf" + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.2" + }, + "timestamp": "2025-06-08T13:06:43.025228" + } +} \ No newline at end of file diff --git a/modules/nf-core/fgbio/copyumifromreadname/environment.yml b/modules/nf-core/fgbio/copyumifromreadname/environment.yml new file mode 100644 index 00000000..4ebc0924 --- /dev/null +++ b/modules/nf-core/fgbio/copyumifromreadname/environment.yml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - bioconda::fgbio=2.4.0 diff --git a/modules/nf-core/fgbio/copyumifromreadname/main.nf b/modules/nf-core/fgbio/copyumifromreadname/main.nf new file mode 100644 index 00000000..b15c970a --- /dev/null +++ b/modules/nf-core/fgbio/copyumifromreadname/main.nf @@ -0,0 +1,64 @@ +process FGBIO_COPYUMIFROMREADNAME { + tag "$meta.id" + label 'process_low' + + conda "${moduleDir}/environment.yml" + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/87/87626ef674e2f19366ae6214575a114fe80ce598e796894820550731706a84be/data' : + 'community.wave.seqera.io/library/fgbio:2.4.0--913bad9d47ff8ddc' }" + + input: + tuple val(meta), path(bam), path(bai) + + output: + tuple val(meta), path("*.bam"), emit: bam + tuple val(meta), path("*.bai"), emit: bai + 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}_umi_extracted" + def mem_gb = 8 + if (!task.memory) { + log.info '[fgbio CopyUmiFromReadName] Available memory not known - defaulting to 8GB. Specify process memory requirements to change this.' + } else if (mem_gb > task.memory.giga) { + if (task.memory.giga < 2) { + mem_gb = 1 + } else { + mem_gb = task.memory.giga - 1 + } + } + """ + fgbio \\ + -Xmx${mem_gb}g \\ + --tmp-dir=. \\ + --async-io=true \\ + CopyUmiFromReadName \\ + ${args} \\ + --input ${bam} \\ + --output ${prefix}.bam + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + fgbio: \$( echo \$(fgbio --version 2>&1 | tr -d '[:cntrl:]' ) | sed -e 's/^.*Version: //;s/\\[.*\$//') + END_VERSIONS + """ + + + stub: + def prefix = task.ext.prefix ?: "${meta.id}_umi_extracted" + """ + + touch ${prefix}.bam + touch ${prefix}.bai + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + fgbio: \$(fgbio --version) + END_VERSIONS + """ +} diff --git a/modules/nf-core/fgbio/copyumifromreadname/meta.yml b/modules/nf-core/fgbio/copyumifromreadname/meta.yml new file mode 100644 index 00000000..7cf4c994 --- /dev/null +++ b/modules/nf-core/fgbio/copyumifromreadname/meta.yml @@ -0,0 +1,80 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/meta-schema.json +name: "fgbio_copyumifromreadname" +description: Copies the UMI at the end of a bam files read name to the RX tag. +keywords: + - fgbio + - copy + - umi + - readname +tools: + - "fgbio": + description: "A set of tools for working with genomic and high throughput sequencing + data, including UMIs" + homepage: http://fulcrumgenomics.github.io/fgbio/ + documentation: http://fulcrumgenomics.github.io/fgbio/tools/latest/CallDuplexConsensusReads.html + tool_dev_url: https://github.com/fulcrumgenomics/fgbio + licence: ["MIT"] + identifier: biotools:fgbio + +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'sample1' ]` + + - bam: + type: file + description: Sorted BAM/CRAM/SAM file + pattern: "*.{bam,cram,sam}" + ontologies: + - edam: "http://edamontology.org/format_2572" # BAM + - edam: "http://edamontology.org/format_2573" # CRAM + - edam: "http://edamontology.org/format_3462" # SAM + + - bai: + type: file + description: Index for bam file + pattern: "*.{bai}" + ontologies: + - edam: "http://edamontology.org/format_2572" # BAM + - edam: "http://edamontology.org/format_2573" # CRAM + - edam: "http://edamontology.org/format_3462" # SAM + +output: + bam: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'sample1' ]` + - "*.bam": + type: file + description: Sorted BAM file + pattern: "*.{bam}" + ontologies: + - edam: "http://edamontology.org/format_2572" # BAM + bai: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'sample1' ]` + - "*.bai": + type: file + description: Index for bam file + pattern: "*.{bai}" + ontologies: + - edam: "http://edamontology.org/format_3327" # BAI + versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" + + ontologies: + - edam: http://edamontology.org/format_3750 # YAML +authors: + - "@sppearce" +maintainers: + - "@sppearce" diff --git a/modules/nf-core/fgbio/copyumifromreadname/tests/main.nf.test b/modules/nf-core/fgbio/copyumifromreadname/tests/main.nf.test new file mode 100644 index 00000000..83d67a42 --- /dev/null +++ b/modules/nf-core/fgbio/copyumifromreadname/tests/main.nf.test @@ -0,0 +1,75 @@ +nextflow_process { + + name "Test Process FGBIO_COPYUMIFROMREADNAME" + script "../main.nf" + process "FGBIO_COPYUMIFROMREADNAME" + + tag "modules" + tag "modules_nfcore" + tag "fgbio" + tag "fgbio/copyumifromreadname" + config "./nextflow.config" + + test("sarscov2 - bam") { + + when { + params { + module_args = '--field-delimiter "_" ' + } + process { + """ + input[0] = [ + [ id:'test'], + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/bam/test.paired_end.umi.sorted.bam', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/bam/test.paired_end.umi.sorted.bam.bai', checkIfExists: true), + ] + """ + } + } + + then { + assert process.success + assertAll( + { assert snapshot( + process.out, + path(process.out.versions[0]).yaml + ).match() + } + ) + } + + } + + test("sarscov2 - bam - stub") { + + options "-stub" + + when { + params { + module_args = '' + } + process { + """ + input[0] = [ + [ id:'test'], + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/bam/test.paired_end.umi.sorted.bam', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/bam/test.paired_end.umi.sorted.bam.bai', checkIfExists: true), + ] + """ + } + } + + then { + assert process.success + assertAll( + { assert snapshot( + process.out, + path(process.out.versions[0]).yaml + ).match() + } + ) + } + + } + +} diff --git a/modules/nf-core/fgbio/copyumifromreadname/tests/main.nf.test.snap b/modules/nf-core/fgbio/copyumifromreadname/tests/main.nf.test.snap new file mode 100644 index 00000000..d65ff345 --- /dev/null +++ b/modules/nf-core/fgbio/copyumifromreadname/tests/main.nf.test.snap @@ -0,0 +1,110 @@ +{ + "sarscov2 - bam - stub": { + "content": [ + { + "0": [ + [ + { + "id": "test" + }, + "test_umi_extracted.bam:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "1": [ + [ + { + "id": "test" + }, + "test_umi_extracted.bai:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "2": [ + "versions.yml:md5,1440a1d99b4c503c037f5325445eb7e6" + ], + "bai": [ + [ + { + "id": "test" + }, + "test_umi_extracted.bai:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "bam": [ + [ + { + "id": "test" + }, + "test_umi_extracted.bam:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "versions": [ + "versions.yml:md5,1440a1d99b4c503c037f5325445eb7e6" + ] + }, + { + "FGBIO_COPYUMIFROMREADNAME": { + "fgbio": null + } + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "24.10.5" + }, + "timestamp": "2025-04-21T10:27:36.454432228" + }, + "sarscov2 - bam": { + "content": [ + { + "0": [ + [ + { + "id": "test" + }, + "test_umi_extracted.bam:md5,245b5f4c2002dc6560353c5183247df3" + ] + ], + "1": [ + [ + { + "id": "test" + }, + "test_umi_extracted.bai:md5,d99827a46b6de71e2338f59eb69b13fc" + ] + ], + "2": [ + "versions.yml:md5,047dd6edb85ae3f51a255523a2bfcfc6" + ], + "bai": [ + [ + { + "id": "test" + }, + "test_umi_extracted.bai:md5,d99827a46b6de71e2338f59eb69b13fc" + ] + ], + "bam": [ + [ + { + "id": "test" + }, + "test_umi_extracted.bam:md5,245b5f4c2002dc6560353c5183247df3" + ] + ], + "versions": [ + "versions.yml:md5,047dd6edb85ae3f51a255523a2bfcfc6" + ] + }, + { + "FGBIO_COPYUMIFROMREADNAME": { + "fgbio": "2.4.0" + } + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "24.10.5" + }, + "timestamp": "2025-04-21T10:27:16.185419539" + } +} \ No newline at end of file diff --git a/modules/nf-core/fgbio/copyumifromreadname/tests/nextflow.config b/modules/nf-core/fgbio/copyumifromreadname/tests/nextflow.config new file mode 100644 index 00000000..d6d31951 --- /dev/null +++ b/modules/nf-core/fgbio/copyumifromreadname/tests/nextflow.config @@ -0,0 +1,5 @@ +process { + withName: "FGBIO_COPYUMIFROMREADNAME" { + ext.args = params.module_args + } +} diff --git a/modules/nf-core/fgbio/fastqtobam/environment.yml b/modules/nf-core/fgbio/fastqtobam/environment.yml new file mode 100644 index 00000000..4dbb6856 --- /dev/null +++ b/modules/nf-core/fgbio/fastqtobam/environment.yml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - bioconda::fgbio=2.5.21 diff --git a/modules/nf-core/fgbio/fastqtobam/main.nf b/modules/nf-core/fgbio/fastqtobam/main.nf new file mode 100644 index 00000000..6ee64bb3 --- /dev/null +++ b/modules/nf-core/fgbio/fastqtobam/main.nf @@ -0,0 +1,70 @@ +process FGBIO_FASTQTOBAM { + tag "$meta.id" + label 'process_low' + + conda "${moduleDir}/environment.yml" + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/b4/b4047e3e517b57fae311eab139a12f0887d898b7da5fceeb2a1029c73b9e3904/data' : + 'community.wave.seqera.io/library/fgbio:2.5.21--368dab1b4f308243' }" + + input: + tuple val(meta), path(reads) + + output: + tuple val(meta), path("*.bam") , emit: bam , optional: true + tuple val(meta), path("*.cram"), emit: cram, optional: true + 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}" + def suffix = task.ext.suffix ?: "bam" + def sample_name = args.contains("--sample") ? "" : "--sample ${prefix}" + def library_name = args.contains("--library") ? "" : "--library ${prefix}" + + def mem_gb = 8 + if (!task.memory) { + log.info '[fgbio FastqToBam] Available memory not known - defaulting to 8GB. Specify process memory requirements to change this.' + } else if (mem_gb > task.memory.giga) { + if (task.memory.giga < 2) { + mem_gb = 1 + } else { + mem_gb = task.memory.giga - 1 + } + } + + """ + fgbio \\ + -Xmx${mem_gb}g \\ + --tmp-dir=. \\ + --async-io=true \\ + FastqToBam \\ + ${args} \\ + --input ${reads} \\ + --output ${prefix}.${suffix} \\ + ${sample_name} \\ + ${library_name} + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + fgbio: \$( echo \$(fgbio --version 2>&1 | tr -d '[:cntrl:]' ) | sed -e 's/^.*Version: //;s/\\[.*\$//') + END_VERSIONS + """ + + stub: + def args = task.ext.args ?: '' + def prefix = task.ext.prefix ?: "${meta.id}" + def suffix = task.ext.suffix ?: "bam" + + """ + touch ${prefix}.${suffix} + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + fgbio: \$( echo \$(fgbio --version 2>&1 | tr -d '[:cntrl:]' ) | sed -e 's/^.*Version: //;s/\\[.*\$//') + END_VERSIONS + """ +} diff --git a/modules/nf-core/fgbio/fastqtobam/meta.yml b/modules/nf-core/fgbio/fastqtobam/meta.yml new file mode 100644 index 00000000..d92f0a60 --- /dev/null +++ b/modules/nf-core/fgbio/fastqtobam/meta.yml @@ -0,0 +1,65 @@ +name: fgbio_fastqtobam +description: | + Using the fgbio tools, converts FASTQ files sequenced into unaligned BAM or CRAM files possibly moving the UMI barcode into the RX field of the reads +keywords: + - unaligned + - bam + - cram +tools: + - fgbio: + description: A set of tools for working with genomic and high throughput sequencing + data, including UMIs + homepage: http://fulcrumgenomics.github.io/fgbio/ + documentation: http://fulcrumgenomics.github.io/fgbio/tools/latest/ + tool_dev_url: https://github.com/fulcrumgenomics/fgbio + licence: ["MIT"] + identifier: biotools:fgbio +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - reads: + type: file + description: pair of reads to be converted into BAM file + pattern: "*.{fastq.gz}" + ontologies: [] +output: + bam: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - "*.bam": + type: file + description: Unaligned, unsorted BAM file + pattern: "*.{bam}" + ontologies: [] + cram: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - "*.cram": + type: file + description: Unaligned, unsorted CRAM file + pattern: "*.{cram}" + ontologies: [] + versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" + ontologies: + - edam: http://edamontology.org/format_3750 # YAML +authors: + - "@lescai" + - "@matthdsm" + - "@nvnieuwk" +maintainers: + - "@lescai" + - "@matthdsm" + - "@nvnieuwk" diff --git a/modules/nf-core/fgbio/fastqtobam/tests/bam.config b/modules/nf-core/fgbio/fastqtobam/tests/bam.config new file mode 100644 index 00000000..014ba920 --- /dev/null +++ b/modules/nf-core/fgbio/fastqtobam/tests/bam.config @@ -0,0 +1,3 @@ +process { + ext.suffix = "bam" +} \ No newline at end of file diff --git a/modules/nf-core/fgbio/fastqtobam/tests/cram.config b/modules/nf-core/fgbio/fastqtobam/tests/cram.config new file mode 100644 index 00000000..2406cb99 --- /dev/null +++ b/modules/nf-core/fgbio/fastqtobam/tests/cram.config @@ -0,0 +1,3 @@ +process { + ext.suffix = "cram" +} \ No newline at end of file diff --git a/modules/nf-core/fgbio/fastqtobam/tests/custom_sample.config b/modules/nf-core/fgbio/fastqtobam/tests/custom_sample.config new file mode 100644 index 00000000..2ed567b4 --- /dev/null +++ b/modules/nf-core/fgbio/fastqtobam/tests/custom_sample.config @@ -0,0 +1,3 @@ +process { + ext.args = "--sample CustomSample --library CustomLibrary" +} \ No newline at end of file diff --git a/modules/nf-core/fgbio/fastqtobam/tests/main.nf.test b/modules/nf-core/fgbio/fastqtobam/tests/main.nf.test new file mode 100644 index 00000000..d10a0052 --- /dev/null +++ b/modules/nf-core/fgbio/fastqtobam/tests/main.nf.test @@ -0,0 +1,218 @@ +nextflow_process { + + name "Test Process FGBIO_FASTQTOBAM" + script "../main.nf" + process "FGBIO_FASTQTOBAM" + + tag "modules" + tag "modules_nfcore" + tag "fgbio" + tag "fgbio/fastqtobam" + + test("homo_sapiens - [fastq1, fastq2] - default") { + + when { + process { + """ + input[0] = [ + [ id:'test', single_end:false ], // meta map + [ + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/fastq/test.umi_1.fastq.gz', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/fastq/test.umi_2.fastq.gz', checkIfExists: true) + ] + ] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot( + file(process.out.bam[0][1]).name, + process.out.cram, + process.out.versions + ).match() } + ) + } + + } + + test("homo_sapiens - [fastq1, fastq2] - cram") { + + config "./cram.config" + when { + process { + """ + input[0] = [ + [ id:'test', single_end:false ], // meta map + [ + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/fastq/test.umi_1.fastq.gz', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/fastq/test.umi_2.fastq.gz', checkIfExists: true) + ] + ] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot( + process.out.bam, + file(process.out.cram[0][1]).name, + process.out.versions + ).match() } + ) + } + + } + + test("homo_sapiens - [fastq1, fastq2] - bam") { + + config "./bam.config" + when { + process { + """ + input[0] = [ + [ id:'test', single_end:false ], // meta map + [ + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/fastq/test.umi_1.fastq.gz', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/fastq/test.umi_2.fastq.gz', checkIfExists: true) + ] + ] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot( + file(process.out.bam[0][1]).name, + process.out.cram, + process.out.versions + ).match() } + ) + } + + } + + test("homo_sapiens - fastq1") { + + when { + process { + """ + input[0] = [ + [ id:'test', single_end:false ], // meta map + [ + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/fastq/test.umi_1.fastq.gz', checkIfExists: true) + ] + ] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot( + file(process.out.bam[0][1]).name, + process.out.cram, + process.out.versions + ).match() } + ) + } + + } + + test("homo_sapiens - [fastq1, fastq2] - umi") { + + config "./umi.config" + when { + process { + """ + input[0] = [ + [ id:'test', single_end:false ], // meta map + [ + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/fastq/test.umi_1.fastq.gz', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/fastq/test.umi_2.fastq.gz', checkIfExists: true) + ] + ] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot( + file(process.out.bam[0][1]).name, + process.out.cram, + process.out.versions + ).match() } + ) + } + + } + + test("homo_sapiens - [fastq1, fastq2] - custom sample") { + + config "./custom_sample.config" + when { + process { + """ + input[0] = [ + [ id:'test', single_end:false ], // meta map + [ + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/fastq/test.umi_1.fastq.gz', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/fastq/test.umi_2.fastq.gz', checkIfExists: true) + ] + ] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot( + file(process.out.bam[0][1]).name, + process.out.cram, + process.out.versions + ).match() } + ) + } + + } + + test("homo_sapiens - [fastq1, fastq2] - stub") { + + options "-stub" + when { + process { + """ + input[0] = [ + [ id:'test', single_end:false ], // meta map + [ + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/fastq/test.umi_1.fastq.gz', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/fastq/test.umi_2.fastq.gz', checkIfExists: true) + ] + ] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot( + file(process.out.bam[0][1]).name, + process.out.cram, + process.out.versions + ).match() } + ) + } + + } +} diff --git a/modules/nf-core/fgbio/fastqtobam/tests/main.nf.test.snap b/modules/nf-core/fgbio/fastqtobam/tests/main.nf.test.snap new file mode 100644 index 00000000..cc01344d --- /dev/null +++ b/modules/nf-core/fgbio/fastqtobam/tests/main.nf.test.snap @@ -0,0 +1,114 @@ +{ + "homo_sapiens - [fastq1, fastq2] - cram": { + "content": [ + [ + + ], + "test.cram", + [ + "versions.yml:md5,468bbf74a89c7db86a209ad9bbfa7736" + ] + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.2" + }, + "timestamp": "2025-06-05T16:33:29.120923" + }, + "homo_sapiens - fastq1": { + "content": [ + "test.bam", + [ + + ], + [ + "versions.yml:md5,468bbf74a89c7db86a209ad9bbfa7736" + ] + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.2" + }, + "timestamp": "2025-06-05T16:34:06.610383" + }, + "homo_sapiens - [fastq1, fastq2] - default": { + "content": [ + "test.bam", + [ + + ], + [ + "versions.yml:md5,468bbf74a89c7db86a209ad9bbfa7736" + ] + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.2" + }, + "timestamp": "2025-06-05T16:33:02.837327" + }, + "homo_sapiens - [fastq1, fastq2] - umi": { + "content": [ + "test.bam", + [ + + ], + [ + "versions.yml:md5,468bbf74a89c7db86a209ad9bbfa7736" + ] + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.2" + }, + "timestamp": "2025-06-05T16:34:25.224411" + }, + "homo_sapiens - [fastq1, fastq2] - bam": { + "content": [ + "test.bam", + [ + + ], + [ + "versions.yml:md5,468bbf74a89c7db86a209ad9bbfa7736" + ] + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.2" + }, + "timestamp": "2025-06-05T16:33:47.975145" + }, + "homo_sapiens - [fastq1, fastq2] - custom sample": { + "content": [ + "test.bam", + [ + + ], + [ + "versions.yml:md5,468bbf74a89c7db86a209ad9bbfa7736" + ] + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.2" + }, + "timestamp": "2025-06-05T16:34:43.606837" + }, + "homo_sapiens - [fastq1, fastq2] - stub": { + "content": [ + "test.bam", + [ + + ], + [ + "versions.yml:md5,468bbf74a89c7db86a209ad9bbfa7736" + ] + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.2" + }, + "timestamp": "2025-06-05T16:34:57.596241" + } +} \ No newline at end of file diff --git a/modules/nf-core/fgbio/fastqtobam/tests/umi.config b/modules/nf-core/fgbio/fastqtobam/tests/umi.config new file mode 100644 index 00000000..7b668aa9 --- /dev/null +++ b/modules/nf-core/fgbio/fastqtobam/tests/umi.config @@ -0,0 +1,3 @@ +process { + ext.args = "--read-structures +T 12M11S+T" +} \ No newline at end of file diff --git a/modules/nf-core/fgbio/filterconsensusreads/environment.yml b/modules/nf-core/fgbio/filterconsensusreads/environment.yml new file mode 100644 index 00000000..4dbb6856 --- /dev/null +++ b/modules/nf-core/fgbio/filterconsensusreads/environment.yml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - bioconda::fgbio=2.5.21 diff --git a/modules/nf-core/fgbio/filterconsensusreads/main.nf b/modules/nf-core/fgbio/filterconsensusreads/main.nf new file mode 100644 index 00000000..717a2587 --- /dev/null +++ b/modules/nf-core/fgbio/filterconsensusreads/main.nf @@ -0,0 +1,71 @@ +process FGBIO_FILTERCONSENSUSREADS { + tag "$meta.id" + label 'process_single' + + conda "${moduleDir}/environment.yml" + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/b4/b4047e3e517b57fae311eab139a12f0887d898b7da5fceeb2a1029c73b9e3904/data' : + 'community.wave.seqera.io/library/fgbio:2.5.21--368dab1b4f308243' }" + + input: + tuple val(meta), path(bam) + tuple val(meta2), path(fasta) + val(min_reads) + val(min_baseq) + val(max_base_error_rate) + + output: + tuple val(meta), path("${prefix}.bam"), emit: bam + path "versions.yml" , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + def args = task.ext.args ?: '' + prefix = task.ext.prefix ?: "${meta.id}_consensus_filtered" + + def mem_gb = 8 + if (!task.memory) { + log.info '[fgbio FilterConsensusReads] Available memory not known - defaulting to 8GB. Specify process memory requirements to change this.' + } else if (mem_gb > task.memory.giga) { + if (task.memory.giga < 2) { + mem_gb = 1 + } else { + mem_gb = task.memory.giga - 1 + } + } + if ("$bam" == "${prefix}.bam") error "Input and output names are the same, use \"task.ext.prefix\" to disambiguate!" + + """ + fgbio \\ + -Xmx${mem_gb}g \\ + --tmp-dir=. \\ + --compression=0 \\ + FilterConsensusReads \\ + --input $bam \\ + --output ${prefix}.bam \\ + --ref ${fasta} \\ + --min-reads ${min_reads} \\ + --min-base-quality ${min_baseq} \\ + --max-base-error-rate ${max_base_error_rate} \\ + $args + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + fgbio: \$( echo \$(fgbio --version 2>&1 | tr -d '[:cntrl:]' ) | sed -e 's/^.*Version: //;s/\\[.*\$//') + END_VERSIONS + """ + + stub: + prefix = task.ext.prefix ?: "${meta.id}_consensus_filtered" + if ("$bam" == "${prefix}.bam") error "Input and output names are the same, use \"task.ext.prefix\" to disambiguate!" + """ + touch ${prefix}.bam + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + fgbio: \$( echo \$(fgbio --version 2>&1 | tr -d '[:cntrl:]' ) | sed -e 's/^.*Version: //;s/\\[.*\$//') + END_VERSIONS + """ +} diff --git a/modules/nf-core/fgbio/filterconsensusreads/meta.yml b/modules/nf-core/fgbio/filterconsensusreads/meta.yml new file mode 100644 index 00000000..17c4f8a1 --- /dev/null +++ b/modules/nf-core/fgbio/filterconsensusreads/meta.yml @@ -0,0 +1,74 @@ +name: "fgbio_filterconsensusreads" +description: Uses FGBIO FilterConsensusReads to filter consensus reads generated by + CallMolecularConsensusReads or CallDuplexConsensusReads. +keywords: + - fgbio + - filter + - consensus + - umi + - duplexumi +tools: + - "fgbio": + description: "A set of tools for working with genomic and high throughput sequencing + data, including UMIs" + homepage: http://fulcrumgenomics.github.io/fgbio/ + documentation: http://fulcrumgenomics.github.io/fgbio/tools/latest/FilterConsensusReads.html + tool_dev_url: https://github.com/fulcrumgenomics/fgbio + licence: ["MIT"] + identifier: biotools:fgbio +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - bam: + type: file + description: BAM file + pattern: "*.bam" + ontologies: [] + - - meta2: + type: map + description: | + Groovy Map containing genome information + e.g. [ id:'test', single_end:false ] + - fasta: + type: file + description: Fasta file containing genomic sequence information + pattern: "*.bam" + ontologies: [] + - min_reads: + type: integer + description: Minimum number of reads required to keep a consensus read + - min_baseq: + type: file + description: Minimum base quality to consider + ontologies: [] + - max_base_error_rate: + type: file + description: Maximum base error rate for a position before it is replaced with + an N. + ontologies: [] +output: + bam: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - ${prefix}.bam: + type: file + description: Filtered consensus BAM file + pattern: "*.bam" + ontologies: [] + versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" + ontologies: + - edam: http://edamontology.org/format_3750 # YAML +authors: + - "@lescai" +maintainers: + - "@lescai" diff --git a/modules/nf-core/fgbio/filterconsensusreads/tests/main.nf.test b/modules/nf-core/fgbio/filterconsensusreads/tests/main.nf.test new file mode 100644 index 00000000..e4f3511f --- /dev/null +++ b/modules/nf-core/fgbio/filterconsensusreads/tests/main.nf.test @@ -0,0 +1,69 @@ +nextflow_process { + + name "Test Process FGBIO_FILTERCONSENSUSREADS" + script "../main.nf" + + process "FGBIO_FILTERCONSENSUSREADS" + + tag "modules" + tag "modules_nfcore" + tag "fgbio" + tag "fgbio/filterconsensusreads" + + test("sarscov2 - bam") { + + when { + process { + """ + input[0] = [[ id:'test', single_end:false ], // meta map + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/bam/umi/test.paired_end.duplex_umi_duplex_consensus.bam', checkIfExists: true) + ] + input[1] = [[ id:'homo_sapiens'], + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta', checkIfExists: true) + ] + input[2] = 3 + input[3] = 45 + input[4] = 0.2 + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + + test("sarscov2 - bam - stub") { + + options "-stub" + + when { + process { + """ + input[0] = [[ id:'test', single_end:false ], // meta map + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/bam/umi/test.paired_end.duplex_umi_duplex_consensus.bam', checkIfExists: true) + ] + input[1] = [[ id:'homo_sapiens'], + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta', checkIfExists: true) + ] + input[2] = 3 + input[3] = 45 + input[4] = 0.2 + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + +} diff --git a/modules/nf-core/fgbio/filterconsensusreads/tests/main.nf.test.snap b/modules/nf-core/fgbio/filterconsensusreads/tests/main.nf.test.snap new file mode 100644 index 00000000..4dff39ad --- /dev/null +++ b/modules/nf-core/fgbio/filterconsensusreads/tests/main.nf.test.snap @@ -0,0 +1,72 @@ +{ + "sarscov2 - bam - stub": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test_consensus_filtered.bam:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "1": [ + "versions.yml:md5,be19391d55fe52c0fd32a844b1aceeb1" + ], + "bam": [ + [ + { + "id": "test", + "single_end": false + }, + "test_consensus_filtered.bam:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "versions": [ + "versions.yml:md5,be19391d55fe52c0fd32a844b1aceeb1" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.2" + }, + "timestamp": "2025-06-06T16:37:18.521589" + }, + "sarscov2 - bam": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test_consensus_filtered.bam:md5,3d3c128a00a1e3c466275516f345daac" + ] + ], + "1": [ + "versions.yml:md5,be19391d55fe52c0fd32a844b1aceeb1" + ], + "bam": [ + [ + { + "id": "test", + "single_end": false + }, + "test_consensus_filtered.bam:md5,3d3c128a00a1e3c466275516f345daac" + ] + ], + "versions": [ + "versions.yml:md5,be19391d55fe52c0fd32a844b1aceeb1" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.2" + }, + "timestamp": "2025-06-06T16:37:04.297362" + } +} \ No newline at end of file diff --git a/modules/nf-core/fgbio/groupreadsbyumi/environment.yml b/modules/nf-core/fgbio/groupreadsbyumi/environment.yml new file mode 100644 index 00000000..4dbb6856 --- /dev/null +++ b/modules/nf-core/fgbio/groupreadsbyumi/environment.yml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - bioconda::fgbio=2.5.21 diff --git a/modules/nf-core/fgbio/groupreadsbyumi/main.nf b/modules/nf-core/fgbio/groupreadsbyumi/main.nf new file mode 100644 index 00000000..f7725219 --- /dev/null +++ b/modules/nf-core/fgbio/groupreadsbyumi/main.nf @@ -0,0 +1,70 @@ +process FGBIO_GROUPREADSBYUMI { + tag "$meta.id" + label 'process_low' + + conda "${moduleDir}/environment.yml" + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/b4/b4047e3e517b57fae311eab139a12f0887d898b7da5fceeb2a1029c73b9e3904/data' : + 'community.wave.seqera.io/library/fgbio:2.5.21--368dab1b4f308243' }" + + input: + tuple val(meta), path(bam) + val(strategy) + + output: + tuple val(meta), path("*.bam") , emit: bam + tuple val(meta), path("*histogram.txt") , emit: histogram + tuple val(meta), path("*read-metrics.txt"), emit: read_metrics + 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}_umi-grouped" + def mem_gb = 8 + if (!task.memory) { + log.info '[fgbio FilterConsensusReads] Available memory not known - defaulting to 8GB. Specify process memory requirements to change this.' + } else if (mem_gb > task.memory.giga) { + if (task.memory.giga < 2) { + mem_gb = 1 + } else { + mem_gb = task.memory.giga - 1 + } + } + + if ("$bam" == "${prefix}.bam") error "Input and output names are the same, use \"task.ext.prefix\" to disambiguate!" + + """ + fgbio \\ + -Xmx${mem_gb}g \\ + --tmp-dir=. \\ + GroupReadsByUmi \\ + -s $strategy \\ + $args \\ + -i $bam \\ + -o ${prefix}.bam \\ + -f ${prefix}_histogram.txt \\ + --grouping-metrics ${prefix}_read-metrics.txt + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + fgbio: \$( echo \$(fgbio --version 2>&1 | tr -d '[:cntrl:]' ) | sed -e 's/^.*Version: //;s/\\[.*\$//') + END_VERSIONS + """ + + stub: + def prefix = task.ext.prefix ?: "${meta.id}_umi-grouped" + if ("$bam" == "${prefix}.bam") error "Input and output names are the same, use \"task.ext.prefix\" to disambiguate!" + """ + touch ${prefix}.bam + touch ${prefix}_histogram.txt + touch ${prefix}_read-metrics.txt + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + fgbio: \$( echo \$(fgbio --version 2>&1 | tr -d '[:cntrl:]' ) | sed -e 's/^.*Version: //;s/\\[.*\$//') + END_VERSIONS + """ +} diff --git a/modules/nf-core/fgbio/groupreadsbyumi/meta.yml b/modules/nf-core/fgbio/groupreadsbyumi/meta.yml new file mode 100644 index 00000000..eb22ec10 --- /dev/null +++ b/modules/nf-core/fgbio/groupreadsbyumi/meta.yml @@ -0,0 +1,84 @@ +name: fgbio_groupreadsbyumi +description: | + Groups reads together that appear to have come from the same original molecule. + Reads are grouped by template, and then templates are sorted by the 5’ mapping positions + of the reads from the template, used from earliest mapping position to latest. + Reads that have the same end positions are then sub-grouped by UMI sequence. + (!) Note: the MQ tag is required on reads with mapped mates (!) + This can be added using samblaster with the optional argument --addMateTags. +keywords: + - UMI + - groupreads + - fgbio +tools: + - fgbio: + description: A set of tools for working with genomic and high throughput sequencing + data, including UMIs + homepage: http://fulcrumgenomics.github.io/fgbio/ + documentation: http://fulcrumgenomics.github.io/fgbio/tools/latest/ + tool_dev_url: https://github.com/fulcrumgenomics/fgbio + licence: ["MIT"] + identifier: biotools:fgbio +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - bam: + type: file + description: | + BAM file. Note: the MQ tag is required on reads with mapped mates (!) + pattern: "*.bam" + ontologies: [] + - strategy: + type: string + enum: ["Identity", "Edit", "Adjacency", "Paired"] + description: | + Required argument: defines the UMI assignment strategy. + Must be chosen among: Identity, Edit, Adjacency, Paired. +output: + bam: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - "*.bam": + type: file + description: UMI-grouped BAM + pattern: "*.bam" + ontologies: [] + histogram: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - "*histogram.txt": + type: file + description: A text file containing the tag family size counts + pattern: "*.txt" + ontologies: [] + read_metrics: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - "*read-metrics.txt": + type: file + description: A text file containing the read count metrics from grouping + pattern: "*.txt" + ontologies: [] + versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" + ontologies: + - edam: http://edamontology.org/format_3750 # YAML +authors: + - "@lescai" +maintainers: + - "@lescai" diff --git a/modules/nf-core/fgbio/groupreadsbyumi/tests/main.nf.test b/modules/nf-core/fgbio/groupreadsbyumi/tests/main.nf.test new file mode 100644 index 00000000..a9e8bd25 --- /dev/null +++ b/modules/nf-core/fgbio/groupreadsbyumi/tests/main.nf.test @@ -0,0 +1,60 @@ +nextflow_process { + + name "Test Process FGBIO_GROUPREADSBYUMI" + script "../main.nf" + process "FGBIO_GROUPREADSBYUMI" + + tag "modules" + tag "modules_nfcore" + tag "fgbio" + tag "fgbio/groupreadsbyumi" + + test("sarscov2 - bam") { + + when { + process { + """ + input[0] = [ + [ id:'test', single_end:false ], // meta map + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/bam/umi/test.paired_end.unsorted_tagged.bam', checkIfExists: true) + ] + input[1] = "Adjacency" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + + test("sarscov2 - bam - stub") { + + options "-stub" + + when { + process { + """ + input[0] = [ + [ id:'test', single_end:false ], // meta map + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/bam/umi/test.paired_end.unsorted_tagged.bam', checkIfExists: true) + ] + input[1] = "Adjacency" + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + +} diff --git a/modules/nf-core/fgbio/groupreadsbyumi/tests/main.nf.test.snap b/modules/nf-core/fgbio/groupreadsbyumi/tests/main.nf.test.snap new file mode 100644 index 00000000..00de4ac0 --- /dev/null +++ b/modules/nf-core/fgbio/groupreadsbyumi/tests/main.nf.test.snap @@ -0,0 +1,144 @@ +{ + "sarscov2 - bam - stub": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test_umi-grouped.bam:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "1": [ + [ + { + "id": "test", + "single_end": false + }, + "test_umi-grouped_histogram.txt:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "2": [ + [ + { + "id": "test", + "single_end": false + }, + "test_umi-grouped_read-metrics.txt:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "3": [ + "versions.yml:md5,3e8002a4c4eef8dc0a715dd9585eeb5b" + ], + "bam": [ + [ + { + "id": "test", + "single_end": false + }, + "test_umi-grouped.bam:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "histogram": [ + [ + { + "id": "test", + "single_end": false + }, + "test_umi-grouped_histogram.txt:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "read_metrics": [ + [ + { + "id": "test", + "single_end": false + }, + "test_umi-grouped_read-metrics.txt:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "versions": [ + "versions.yml:md5,3e8002a4c4eef8dc0a715dd9585eeb5b" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.2" + }, + "timestamp": "2025-06-06T16:37:53.48947" + }, + "sarscov2 - bam": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test_umi-grouped.bam:md5,35bfc992c30d8e3e50816159fa58cb11" + ] + ], + "1": [ + [ + { + "id": "test", + "single_end": false + }, + "test_umi-grouped_histogram.txt:md5,9a0c622b65209afbce0840e2affff983" + ] + ], + "2": [ + [ + { + "id": "test", + "single_end": false + }, + "test_umi-grouped_read-metrics.txt:md5,a5f75e3e390e30791a636fed355e0afd" + ] + ], + "3": [ + "versions.yml:md5,3e8002a4c4eef8dc0a715dd9585eeb5b" + ], + "bam": [ + [ + { + "id": "test", + "single_end": false + }, + "test_umi-grouped.bam:md5,35bfc992c30d8e3e50816159fa58cb11" + ] + ], + "histogram": [ + [ + { + "id": "test", + "single_end": false + }, + "test_umi-grouped_histogram.txt:md5,9a0c622b65209afbce0840e2affff983" + ] + ], + "read_metrics": [ + [ + { + "id": "test", + "single_end": false + }, + "test_umi-grouped_read-metrics.txt:md5,a5f75e3e390e30791a636fed355e0afd" + ] + ], + "versions": [ + "versions.yml:md5,3e8002a4c4eef8dc0a715dd9585eeb5b" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.2" + }, + "timestamp": "2025-06-10T08:59:32.932448" + } +} \ No newline at end of file diff --git a/modules/nf-core/fgbio/sortbam/environment.yml b/modules/nf-core/fgbio/sortbam/environment.yml new file mode 100644 index 00000000..9645b667 --- /dev/null +++ b/modules/nf-core/fgbio/sortbam/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: + # renovate: datasource=conda depName=bioconda/fgbio + - bioconda::fgbio=2.5.21 diff --git a/modules/nf-core/fgbio/sortbam/main.nf b/modules/nf-core/fgbio/sortbam/main.nf new file mode 100644 index 00000000..3b3e6521 --- /dev/null +++ b/modules/nf-core/fgbio/sortbam/main.nf @@ -0,0 +1,61 @@ +process FGBIO_SORTBAM { + tag "$meta.id" + label 'process_medium' + + conda "${moduleDir}/environment.yml" + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/b4/b4047e3e517b57fae311eab139a12f0887d898b7da5fceeb2a1029c73b9e3904/data' : + 'community.wave.seqera.io/library/fgbio:2.5.21--368dab1b4f308243' }" + + input: + tuple val(meta), path(bam) + + output: + tuple val(meta), path("*.bam"), emit: bam + 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}_sorted" + def mem_gb = 8 + if (!task.memory) { + log.info '[fgbio SortBam] Available memory not known - defaulting to 8GB. Specify process memory requirements to change this.' + } else if (mem_gb > task.memory.giga) { + if (task.memory.giga < 2) { + mem_gb = 1 + } else { + mem_gb = task.memory.giga - 1 + } + } + + if ("$bam" == "${prefix}.bam") error "Input and output names are the same, use \"task.ext.prefix\" to disambiguate!" + + """ + fgbio -Xmx${mem_gb}g \\ + --async-io=true \\ + --tmp-dir=. \\ + SortBam \\ + -i $bam \\ + $args \\ + -o ${prefix}.bam + cat <<-END_VERSIONS > versions.yml + "${task.process}": + fgbio: \$( echo \$(fgbio --version 2>&1 | tr -d '[:cntrl:]' ) | sed -e 's/^.*Version: //;s/\\[.*\$//') + END_VERSIONS + """ + + stub: + prefix = task.ext.prefix ?: "${meta.id}_sorted" + if ("$bam" == "${prefix}.bam") error "Input and output names are the same, use \"task.ext.prefix\" to disambiguate!" + """ + touch ${prefix}.bam + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + fgbio: \$( echo \$(fgbio --version 2>&1 | tr -d '[:cntrl:]' ) | sed -e 's/^.*Version: //;s/\\[.*\$//') + END_VERSIONS + """ +} diff --git a/modules/nf-core/fgbio/sortbam/meta.yml b/modules/nf-core/fgbio/sortbam/meta.yml new file mode 100644 index 00000000..81c295d6 --- /dev/null +++ b/modules/nf-core/fgbio/sortbam/meta.yml @@ -0,0 +1,50 @@ +name: fgbio_sortbam +description: Sorts a SAM or BAM file. Several sort orders are available, including + coordinate, queryname, random, and randomquery. +keywords: + - sort + - bam + - sam +tools: + - fgbio: + description: Tools for working with genomic and high throughput sequencing data. + homepage: https://github.com/fulcrumgenomics/fgbio + documentation: http://fulcrumgenomics.github.io/fgbio/ + licence: ["MIT"] + identifier: biotools:fgbio +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false, collapse:false ] + - bam: + type: file + description: | + The input SAM or BAM file to be sorted. + pattern: "*.{bam,sam}" + ontologies: [] +output: + bam: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - "*.bam": + type: file + description: | + Output SAM or BAM file. + pattern: "*.{bam,sam}" + ontologies: [] + versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" + ontologies: + - edam: http://edamontology.org/format_3750 # YAML +authors: + - "@sruthipsuresh" +maintainers: + - "@sruthipsuresh" diff --git a/modules/nf-core/fgbio/sortbam/tests/main.nf.test b/modules/nf-core/fgbio/sortbam/tests/main.nf.test new file mode 100644 index 00000000..2e9b2459 --- /dev/null +++ b/modules/nf-core/fgbio/sortbam/tests/main.nf.test @@ -0,0 +1,56 @@ +nextflow_process { + + name "Test Process FGBIO_SORTBAM" + script "../main.nf" + process "FGBIO_SORTBAM" + + tag "modules" + tag "modules_nfcore" + tag "fgbio" + tag "fgbio/sortbam" + + test("sarscov2 - bam") { + + when { + process { + """ + input[0] = [ [ id:'test' ], // meta map + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/bam/test.paired_end.sorted.bam', checkIfExists: true) + ] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + + test("sarscov2 - bam - stub") { + + options "-stub" + + when { + process { + """ + input[0] = [ [ id:'test' ], // meta map + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/bam/test.paired_end.sorted.bam', checkIfExists: true) + ] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + +} diff --git a/modules/nf-core/fgbio/sortbam/tests/main.nf.test.snap b/modules/nf-core/fgbio/sortbam/tests/main.nf.test.snap new file mode 100644 index 00000000..cb8d6768 --- /dev/null +++ b/modules/nf-core/fgbio/sortbam/tests/main.nf.test.snap @@ -0,0 +1,68 @@ +{ + "sarscov2 - bam - stub": { + "content": [ + { + "0": [ + [ + { + "id": "test" + }, + "test_sorted.bam:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "1": [ + "versions.yml:md5,c6608b61c38dcf9142a28a0d665eb96d" + ], + "bam": [ + [ + { + "id": "test" + }, + "test_sorted.bam:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "versions": [ + "versions.yml:md5,c6608b61c38dcf9142a28a0d665eb96d" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.2" + }, + "timestamp": "2025-06-06T16:38:27.474292" + }, + "sarscov2 - bam": { + "content": [ + { + "0": [ + [ + { + "id": "test" + }, + "test_sorted.bam:md5,1d7a558a72b7aecc80946cb9cadf8f60" + ] + ], + "1": [ + "versions.yml:md5,c6608b61c38dcf9142a28a0d665eb96d" + ], + "bam": [ + [ + { + "id": "test" + }, + "test_sorted.bam:md5,1d7a558a72b7aecc80946cb9cadf8f60" + ] + ], + "versions": [ + "versions.yml:md5,c6608b61c38dcf9142a28a0d665eb96d" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.2" + }, + "timestamp": "2025-06-06T16:38:12.994113" + } +} \ No newline at end of file diff --git a/modules/nf-core/fgbio/zipperbams/environment.yml b/modules/nf-core/fgbio/zipperbams/environment.yml new file mode 100644 index 00000000..4dbb6856 --- /dev/null +++ b/modules/nf-core/fgbio/zipperbams/environment.yml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - bioconda::fgbio=2.5.21 diff --git a/modules/nf-core/fgbio/zipperbams/main.nf b/modules/nf-core/fgbio/zipperbams/main.nf new file mode 100644 index 00000000..bf523804 --- /dev/null +++ b/modules/nf-core/fgbio/zipperbams/main.nf @@ -0,0 +1,73 @@ +process FGBIO_ZIPPERBAMS { + tag "$meta.id" + label 'process_single' + + conda "${moduleDir}/environment.yml" + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/b4/b4047e3e517b57fae311eab139a12f0887d898b7da5fceeb2a1029c73b9e3904/data' : + 'community.wave.seqera.io/library/fgbio:2.5.21--368dab1b4f308243' }" + + input: + tuple val(meta), path(unmapped_bam) + tuple val(meta2), path(mapped_bam) + tuple val(meta3), path(fasta) + tuple val(meta4), path(dict) + + output: + tuple val(meta), path("${prefix}.bam"), emit: bam + path "versions.yml" , emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + def args = task.ext.args ?: '' + def args2 = task.ext.args2 ?: '' + def compression = task.ext.compression ?: '0' + prefix = task.ext.prefix ?: "${meta.id}_zipped" + def mem_gb = 8 + if (!task.memory) { + log.info '[fgbio ZipperBams] Available memory not known - defaulting to 8GB. Specify process memory requirements to change this.' + } else if (mem_gb > task.memory.giga) { + if (task.memory.giga < 2) { + mem_gb = 1 + } else { + mem_gb = task.memory.giga - 1 + } + } + + if ("${unmapped_bam}" == "${prefix}.bam") error "Input and output names are the same, use \"task.ext.prefix\" to disambiguate!" + if ("${mapped_bam}" == "${prefix}.bam") error "Input and output names are the same, use \"task.ext.prefix\" to disambiguate!" + + """ + fgbio -Xmx${mem_gb}g \\ + --compression ${compression} \\ + --async-io=true \\ + ZipperBams \\ + --unmapped ${unmapped_bam} \\ + --input ${mapped_bam} \\ + --ref ${fasta} \\ + ${args} \\ + --output ${prefix}.bam + + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + fgbio: \$( echo \$(fgbio --version 2>&1 | tr -d '[:cntrl:]' ) | sed -e 's/^.*Version: //;s/\\[.*\$//') + END_VERSIONS + """ + + stub: + prefix = task.ext.prefix ?: "${meta.id}_zipped" + if ("${unmapped_bam}" == "${prefix}.bam") error "Input and output names are the same, use \"task.ext.prefix\" to disambiguate!" + if ("${mapped_bam}" == "${prefix}.bam") error "Input and output names are the same, use \"task.ext.prefix\" to disambiguate!" + + """ + touch ${prefix}.bam + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + fgbio: \$( echo \$(fgbio --version 2>&1 | tr -d '[:cntrl:]' ) | sed -e 's/^.*Version: //;s/\\[.*\$//') + END_VERSIONS + """ +} diff --git a/modules/nf-core/fgbio/zipperbams/meta.yml b/modules/nf-core/fgbio/zipperbams/meta.yml new file mode 100644 index 00000000..052b50e1 --- /dev/null +++ b/modules/nf-core/fgbio/zipperbams/meta.yml @@ -0,0 +1,82 @@ +name: "fgbio_zipperbams" +description: FGBIO tool to zip together an unmapped and mapped BAM to transfer metadata + into the output BAM +keywords: + - fgbio + - umi + - unmapped + - ubam + - zipperbams +tools: + - fgbio: + description: A set of tools for working with genomic and high throughput sequencing + data, including UMIs + homepage: http://fulcrumgenomics.github.io/fgbio/ + documentation: http://fulcrumgenomics.github.io/fgbio/tools/latest/ + tool_dev_url: https://github.com/fulcrumgenomics/fgbio + licence: ["MIT"] + identifier: biotools:fgbio +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - unmapped_bam: + type: file + description: unmapped BAM file + pattern: "*.bam" + ontologies: [] + - - meta2: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - mapped_bam: + type: file + description: mapped BAM/SAM file + pattern: "*.{bam,sam}" + ontologies: [] + - - meta3: + type: map + description: | + Groovy Map containing reference information + e.g. [ id:'GRCh38' ] + - fasta: + type: file + description: fasta file containing genomic sequence information + pattern: "*.{fasta,fa}" + ontologies: [] + - - meta4: + type: map + description: | + Groovy Map containing reference information + e.g. [ id:'GRCh38' ] + - dict: + type: file + description: dict file containing a sequence dictionary for the fasta file + pattern: "*.{dict}" + ontologies: [] +output: + bam: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - ${prefix}.bam: + type: file + description: Zipped BAM file + pattern: "*.bam" + ontologies: [] + versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" + ontologies: + - edam: http://edamontology.org/format_3750 # YAML +authors: + - "@lescai" +maintainers: + - "@lescai" diff --git a/modules/nf-core/fgbio/zipperbams/tests/main.nf.test b/modules/nf-core/fgbio/zipperbams/tests/main.nf.test new file mode 100644 index 00000000..89f7ce5c --- /dev/null +++ b/modules/nf-core/fgbio/zipperbams/tests/main.nf.test @@ -0,0 +1,83 @@ +nextflow_process { + + name "Test Process FGBIO_ZIPPERBAMS" + script "../main.nf" + process "FGBIO_ZIPPERBAMS" + + tag "modules" + tag "modules_nfcore" + tag "fgbio" + tag "fgbio/zipperbams" + + test("sarscov2 - bam") { + config "./nextflow.config" + + when { + process { + """ + input[0] = [ + [ id:'test', single_end:false ], + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/bam/umi/test.paired_end.duplex_umi_unmapped.bam', checkIfExists: true) + ] + input[1] = [ + [ id:'test', single_end:false ], + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/bam/umi/test.paired_end.duplex_umi_mapped.bam', checkIfExists: true) + ] + input[2] = [ + [ id:'test'], + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta', checkIfExists: true) + ] + input[3] = [ + [ id:'test'], + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.dict', checkIfExists: true) + ] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + + test("sarscov2 - bam - stub") { + + options "-stub" + + when { + process { + """ + input[0] = [ + [ id:'test', single_end:false ], + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/bam/umi/test.paired_end.duplex_umi_unmapped.bam', checkIfExists: true) + ] + input[1] = [ + [ id:'test', single_end:false ], + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/bam/umi/test.paired_end.duplex_umi_mapped.bam', checkIfExists: true) + ] + input[2] = [ + [ id:'test'], + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta', checkIfExists: true) + ] + input[3] = [ + [ id:'test'], + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.dict', checkIfExists: true) + ] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + +} diff --git a/modules/nf-core/fgbio/zipperbams/tests/main.nf.test.snap b/modules/nf-core/fgbio/zipperbams/tests/main.nf.test.snap new file mode 100644 index 00000000..9ceb5b24 --- /dev/null +++ b/modules/nf-core/fgbio/zipperbams/tests/main.nf.test.snap @@ -0,0 +1,72 @@ +{ + "sarscov2 - bam - stub": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test_zipped.bam:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "1": [ + "versions.yml:md5,786ad0edcd8c1ead6fd6d8f8a751f971" + ], + "bam": [ + [ + { + "id": "test", + "single_end": false + }, + "test_zipped.bam:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "versions": [ + "versions.yml:md5,786ad0edcd8c1ead6fd6d8f8a751f971" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.2" + }, + "timestamp": "2025-06-06T16:39:17.538398" + }, + "sarscov2 - bam": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test_zipped.bam:md5,1980b44177f4720f1005c9be62b09f79" + ] + ], + "1": [ + "versions.yml:md5,786ad0edcd8c1ead6fd6d8f8a751f971" + ], + "bam": [ + [ + { + "id": "test", + "single_end": false + }, + "test_zipped.bam:md5,1980b44177f4720f1005c9be62b09f79" + ] + ], + "versions": [ + "versions.yml:md5,786ad0edcd8c1ead6fd6d8f8a751f971" + ] + } + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.2" + }, + "timestamp": "2025-06-06T16:38:57.558961" + } +} \ No newline at end of file diff --git a/modules/nf-core/fgbio/zipperbams/tests/nextflow.config b/modules/nf-core/fgbio/zipperbams/tests/nextflow.config new file mode 100644 index 00000000..4c4c4ddc --- /dev/null +++ b/modules/nf-core/fgbio/zipperbams/tests/nextflow.config @@ -0,0 +1,5 @@ +process { + withName: "FGBIO_ZIPPERBAMS" { + ext.args = "--tags-to-reverse Consensus --tags-to-revcomp Consensus" + } +} From 51ca1e7df7123d38902bbfe24768715187d819d3 Mon Sep 17 00:00:00 2001 From: hwjeong Date: Thu, 14 Aug 2025 09:57:52 +0200 Subject: [PATCH 02/33] Update consensus workflow --- subworkflows/local/consensus/main.nf | 71 ++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 subworkflows/local/consensus/main.nf diff --git a/subworkflows/local/consensus/main.nf b/subworkflows/local/consensus/main.nf new file mode 100644 index 00000000..6ac57d5a --- /dev/null +++ b/subworkflows/local/consensus/main.nf @@ -0,0 +1,71 @@ +include { FGBIO_COPYUMIFROMREADNAME } from '../../../modules/nf-core/fgbio/copyumifromreadname/main' +include { FGBIO_CALLDUPLEXCONSENSUSREADS } from '../../../modules/nf-core/fgbio/callduplexconsensusreads/main' +include { FGBIO_CALLMOLECULARCONSENSUSREADS } from '../../../modules/nf-core/fgbio/callmolecularconsensusreads/main' +include { FGBIO_COLLECTDUPLEXSEQMETRICS } from '../../../modules/nf-core/fgbio/collectduplexseqmetrics/main' +include { FGBIO_FASTQTOBAM } from '../../../modules/nf-core/fgbio/fastqtobam/main' +include { FGBIO_FILTERCONSENSUSREADS } from '../../../modules/nf-core/fgbio/filterconsensusreads/main' +include { FGBIO_GROUPREADSBYUMI } from '../../../modules/nf-core/fgbio/groupreadsbyumi/main' +include { FGBIO_SORTBAM } from '../../../modules/nf-core/fgbio/sortbam/main' +include { FGBIO_ZIPPERBAMS } from '../../../modules/nf-core/fgbio/zipperbams/main' + +workflow CONSENSUS { + take: + ch_fastq // channel: tuple(meta, fastq1, fastq2) for SE/PE/duplex samples + ch_reference // channel: reference genome fasta file + ch_umi_in_readname // boolean + + main: + ch_fastq + .combine(ch_umi_in_readname) + .branch { tuple -> + meta, fastq1, fastq2, umi_in_readname = tuple + umi_in_readname: umi_in_readname == true + umi_in_sequence: umi_in_readname == false + } + .set { ch_fastq_branch } + + // UMI is in read name: run CopyUmiFromReadName before FastqToBam + ch_fastq_branch.umi_in_readname + .map { meta, fastq1, fastq2 -> + // Step 1: Extract UMI from read name and copy to BAM tag + COPYUMIFROMREADNAME([meta, fastq1, fastq2]) + // Step 2: Convert FASTQ to BAM (UMI tag already present) + FASTQTOBAM(COPYUMIFROMREADNAME.out.bam, ch_reference, ch_umi_info) + // Step 3: Sort BAM + SORTBAM(FASTQTOBAM.out.bam) + // Step 4: Group reads by UMI + GROUPREADSBYUMI(SORTBAM.out.bam) + // Step 5: Call molecular consensus reads + CALLMOLECULARCONSENSUSREADS(GROUPREADSBYUMI.out.bam) + // Step 6: If duplex UMI, call duplex consensus reads + if (meta.umi_kit == 'DUPLEX') { + CALLDUPLEXCONSENSUSREADS(CALLMOLECULARCONSENSUSREADS.out.bam) + } + // Step 7: Filter consensus reads + FILTERCONSENSUSREADS(...) + // Additional steps as needed + } + + // UMI is in sequence: run FastqToBam directly + ch_fastq_branch.umi_in_sequence + .map { meta, fastq1, fastq2 -> + // Step 1: Convert FASTQ to BAM (UMI extracted from sequence/index) + FASTQTOBAM([meta, fastq1, fastq2], ch_reference, ch_umi_info) + // Step 2: Sort BAM + SORTBAM(FASTQTOBAM.out.bam) + // Step 3: Group reads by UMI + GROUPREADSBYUMI(SORTBAM.out.bam) + // Step 4: Call molecular consensus reads + CALLMOLECULARCONSENSUSREADS(GROUPREADSBYUMI.out.bam) + // Step 5: If duplex UMI, call duplex consensus reads + if (meta.umi_kit == 'DUPLEX') { + CALLDUPLEXCONSENSUSREADS(CALLMOLECULARCONSENSUSREADS.out.bam) + } + // Step 6: Filter consensus reads + FILTERCONSENSUSREADS(...) + // Additional steps as needed + } + + emit: + consensus_bam = ... // Final consensus BAM/CRAM output +} From eff57430f71afe49bf3756a44ab36cb016233c78 Mon Sep 17 00:00:00 2001 From: Dhamar Date: Thu, 14 Aug 2025 14:14:06 +0200 Subject: [PATCH 03/33] main.nf, trying to run tests but still working on it --- nextflow.config | 5 ++ subworkflows/local/consensus/main.nf | 124 ++++++++++++++------------- workflows/preprocessing.nf | 38 +++++++- 3 files changed, 105 insertions(+), 62 deletions(-) diff --git a/nextflow.config b/nextflow.config index cb90bae7..b57269c2 100644 --- a/nextflow.config +++ b/nextflow.config @@ -26,6 +26,11 @@ params { roi = null genelists = null + // --- UMI options --- + enable_umi = false // turn UMI subworkflow on/off + umi_in_readname = true // true: UMI in read name; false: in sequence (future) + enable_duplex = false // run duplex branch (zipper -> DCS) + // References genomes = [:] diff --git a/subworkflows/local/consensus/main.nf b/subworkflows/local/consensus/main.nf index 6ac57d5a..6bdbc2a2 100644 --- a/subworkflows/local/consensus/main.nf +++ b/subworkflows/local/consensus/main.nf @@ -1,71 +1,73 @@ -include { FGBIO_COPYUMIFROMREADNAME } from '../../../modules/nf-core/fgbio/copyumifromreadname/main' -include { FGBIO_CALLDUPLEXCONSENSUSREADS } from '../../../modules/nf-core/fgbio/callduplexconsensusreads/main' -include { FGBIO_CALLMOLECULARCONSENSUSREADS } from '../../../modules/nf-core/fgbio/callmolecularconsensusreads/main' -include { FGBIO_COLLECTDUPLEXSEQMETRICS } from '../../../modules/nf-core/fgbio/collectduplexseqmetrics/main' -include { FGBIO_FASTQTOBAM } from '../../../modules/nf-core/fgbio/fastqtobam/main' -include { FGBIO_FILTERCONSENSUSREADS } from '../../../modules/nf-core/fgbio/filterconsensusreads/main' -include { FGBIO_GROUPREADSBYUMI } from '../../../modules/nf-core/fgbio/groupreadsbyumi/main' -include { FGBIO_SORTBAM } from '../../../modules/nf-core/fgbio/sortbam/main' -include { FGBIO_ZIPPERBAMS } from '../../../modules/nf-core/fgbio/zipperbams/main' +#!/usr/bin/env nextflow + +include { FGBIO_COPYUMIFROMREADNAME } from "${projectDir}/modules/nf-core/fgbio/copyumifromreadname/main.nf" +include { FGBIO_CALLDUPLEXCONSENSUSREADS } from "${projectDir}/modules/nf-core/fgbio/callduplexconsensusreads/main.nf" +include { FGBIO_CALLMOLECULARCONSENSUSREADS } from "${projectDir}/modules/nf-core/fgbio/callmolecularconsensusreads/main.nf" +include { FGBIO_COLLECTDUPLEXSEQMETRICS } from "${projectDir}/modules/nf-core/fgbio/collectduplexseqmetrics/main.nf" +include { FGBIO_FASTQTOBAM } from "${projectDir}/modules/nf-core/fgbio/fastqtobam/main.nf" +include { FGBIO_FILTERCONSENSUSREADS } from "${projectDir}/modules/nf-core/fgbio/filterconsensusreads/main.nf" +include { FGBIO_GROUPREADSBYUMI } from "${projectDir}/modules/nf-core/fgbio/groupreadsbyumi/main.nf" +include { FGBIO_SORTBAM } from "${projectDir}/modules/nf-core/fgbio/sortbam/main.nf" +include { FGBIO_ZIPPERBAMS } from "${projectDir}/modules/nf-core/fgbio/zipperbams/main.nf" workflow CONSENSUS { take: - ch_fastq // channel: tuple(meta, fastq1, fastq2) for SE/PE/duplex samples - ch_reference // channel: reference genome fasta file - ch_umi_in_readname // boolean + ch_fastq // channel: tuple(meta, fastq1, fastq2) for SE/PE/duplex samples + ch_reference // channel: reference genome fasta file + ch_umi_in_readname // boolean main: - ch_fastq - .combine(ch_umi_in_readname) - .branch { tuple -> - meta, fastq1, fastq2, umi_in_readname = tuple - umi_in_readname: umi_in_readname == true - umi_in_sequence: umi_in_readname == false - } - .set { ch_fastq_branch } + ch_versions = Channel.empty() + ch_duplex_metrics = Channel.empty() - // UMI is in read name: run CopyUmiFromReadName before FastqToBam - ch_fastq_branch.umi_in_readname - .map { meta, fastq1, fastq2 -> - // Step 1: Extract UMI from read name and copy to BAM tag - COPYUMIFROMREADNAME([meta, fastq1, fastq2]) - // Step 2: Convert FASTQ to BAM (UMI tag already present) - FASTQTOBAM(COPYUMIFROMREADNAME.out.bam, ch_reference, ch_umi_info) - // Step 3: Sort BAM - SORTBAM(FASTQTOBAM.out.bam) - // Step 4: Group reads by UMI - GROUPREADSBYUMI(SORTBAM.out.bam) - // Step 5: Call molecular consensus reads - CALLMOLECULARCONSENSUSREADS(GROUPREADSBYUMI.out.bam) - // Step 6: If duplex UMI, call duplex consensus reads - if (meta.umi_kit == 'DUPLEX') { - CALLDUPLEXCONSENSUSREADS(CALLMOLECULARCONSENSUSREADS.out.bam) - } - // Step 7: Filter consensus reads - FILTERCONSENSUSREADS(...) - // Additional steps as needed - } + ch_fastq + .combine(ch_umi_in_readname) + .branch( + rn: { it[3] == true }, + seq: { it[3] == false } + ) + .set { ch_fastq_branch } + + RN_FQ = ch_fastq_branch.rn .map { meta, r1, r2, f -> [meta, r1, r2] } + SEQ_FQ = ch_fastq_branch.seq.map { meta, r1, r2, f -> [meta, r1, r2] } + + FGBIO_FASTQTOBAM(RN_FQ, ch_reference) + ch_versions = ch_versions.mix(FGBIO_FASTQTOBAM.out.versions.first()) + + FGBIO_COPYUMIFROMREADNAME(FGBIO_FASTQTOBAM.out.bam) + ch_versions = ch_versions.mix(FGBIO_COPYUMIFROMREADNAME.out.versions.first()) - // UMI is in sequence: run FastqToBam directly - ch_fastq_branch.umi_in_sequence - .map { meta, fastq1, fastq2 -> - // Step 1: Convert FASTQ to BAM (UMI extracted from sequence/index) - FASTQTOBAM([meta, fastq1, fastq2], ch_reference, ch_umi_info) - // Step 2: Sort BAM - SORTBAM(FASTQTOBAM.out.bam) - // Step 3: Group reads by UMI - GROUPREADSBYUMI(SORTBAM.out.bam) - // Step 4: Call molecular consensus reads - CALLMOLECULARCONSENSUSREADS(GROUPREADSBYUMI.out.bam) - // Step 5: If duplex UMI, call duplex consensus reads - if (meta.umi_kit == 'DUPLEX') { - CALLDUPLEXCONSENSUSREADS(CALLMOLECULARCONSENSUSREADS.out.bam) + FGBIO_SORTBAM(FGBIO_COPYUMIFROMREADNAME.out.bam) + ch_versions = ch_versions.mix(FGBIO_SORTBAM.out.versions.first()) + + FGBIO_GROUPREADSBYUMI(FGBIO_SORTBAM.out.bam) + ch_versions = ch_versions.mix(FGBIO_GROUPREADSBYUMI.out.versions.first()) + + FGBIO_CALLMOLECULARCONSENSUSREADS(FGBIO_GROUPREADSBYUMI.out.bam) + ch_versions = ch_versions.mix(FGBIO_CALLMOLECULARCONSENSUSREADS.out.versions.first()) + + if (params.enable_duplex) { + FGBIO_ZIPPERBAMS(FGBIO_CALLMOLECULARCONSENSUSREADS.out.bam) + ch_versions = ch_versions.mix(FGBIO_ZIPPERBAMS.out.versions.first()) + FGBIO_CALLDUPLEXCONSENSUSREADS(FGBIO_ZIPPERBAMS.out.bam) + ch_versions = ch_versions.mix(FGBIO_CALLDUPLEXCONSENSUSREADS.out.versions.first()) + FGBIO_FILTERCONSENSUSREADS(FGBIO_CALLDUPLEXCONSENSUSREADS.out.bam) + ch_versions = ch_versions.mix(FGBIO_FILTERCONSENSUSREADS.out.versions.first()) + FGBIO_COLLECTDUPLEXSEQMETRICS(FGBIO_FILTERCONSENSUSREADS.out.bam) + ch_versions = ch_versions.mix(FGBIO_COLLECTDUPLEXSEQMETRICS.out.versions.first()) + ch_duplex_metrics = ch_duplex_metrics.mix(FGBIO_COLLECTDUPLEXSEQMETRICS.out.metrics) + } else { + FGBIO_FILTERCONSENSUSREADS(FGBIO_CALLMOLECULARCONSENSUSREADS.out.bam) + ch_versions = ch_versions.mix(FGBIO_FILTERCONSENSUSREADS.out.versions.first()) } - // Step 6: Filter consensus reads - FILTERCONSENSUSREADS(...) - // Additional steps as needed - } + + ch_consensus_rn = FGBIO_FILTERCONSENSUSREADS.out.bam + ch_consensus_seq = Channel.empty() // pending: ExtractUmisFromBam branch + + ch_consensus = ch_consensus_rn.mix(ch_consensus_seq) emit: - consensus_bam = ... // Final consensus BAM/CRAM output -} + consensus_bam = ch_consensus + duplex_metrics = ch_duplex_metrics + versions = ch_versions +} \ No newline at end of file diff --git a/workflows/preprocessing.nf b/workflows/preprocessing.nf index d23a17b1..d22e4ae5 100644 --- a/workflows/preprocessing.nf +++ b/workflows/preprocessing.nf @@ -19,6 +19,7 @@ include { BCL_DEMULTIPLEX } from '../subworkflows/nf-core/bcl_demultiplex include { COVERAGE } from '../subworkflows/local/coverage/main' include { FASTQ_TO_UCRAM } from '../subworkflows/local/fastq_to_unaligned_cram/main' include { FASTQ_TO_CRAM } from '../subworkflows/local/fastq_to_aligned_cram/main' +include { CONSENSUS } from '../subworkflows/local/consensus/main' // Functions include { paramsSummaryMap } from 'plugin/nf-schema' @@ -235,6 +236,42 @@ workflow PREPROCESSING { ch_trimmed_reads.other.dump(tag:"Other trimmed reads per sample", pretty: true) +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// STEP: UMI CONSENSUS (optional, before alignment) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +*/ + +UMI_FLAG = Channel.value(params.umi_in_readname as boolean) + +if (params.enable_umi) { + + // ch_trimmed_reads.supported: [meta, reads] + // Convert to [meta, r1, r2] for the UMI subworkflow + CH_UMI_FASTQ = ch_trimmed_reads.supported.map { meta, reads -> + def r1 = (reads instanceof List) ? reads[0] : reads + def r2 = (reads instanceof List && reads.size() > 1) ? reads[1] : null + return [meta, r1, r2] + } + + // Per-sample reference fasta (same that you use later for alignment) + CH_UMI_FASTA = ch_trimmed_reads.supported.map { meta, reads -> + return [meta, getGenomeAttribute(meta.genome_data, "fasta")] + } + + // Call the UMI subworkflow “like a function” + // NOTE: your CONSENSUS expects (ch_fastq, ch_reference, ch_umi_in_readname) + // If your CONSENSUS expects a single fasta (not keyed), replace CH_UMI_FASTA by Channel.fromPath(...). + CONSENSUS(CH_UMI_FASTQ, CH_UMI_FASTA, UMI_FLAG) + + ch_versions = ch_versions.mix(CONSENSUS.out.versions) + + // Output channels + ch_umi_consensus_bam = CONSENSUS.out.consensus_bam + ch_umi_duplex_metrics = CONSENSUS.out.duplex_metrics +} + + /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // STEP: FASTQ TO UNALIGNED CRAM CONVERSION @@ -244,7 +281,6 @@ workflow PREPROCESSING { FASTQ_TO_UCRAM(ch_trimmed_reads.other) ch_versions = ch_versions.mix(FASTQ_TO_UCRAM.out.versions) -/* /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From eeb1d31eddea930d5c9f5a4c48db27e077ca4efd Mon Sep 17 00:00:00 2001 From: hwjeong Date: Tue, 19 Aug 2025 14:46:01 +0200 Subject: [PATCH 04/33] Update on the current situation (schema, config, main, preprocessing) --- modules.json | 10 +++ nextflow.config | 1 - nextflow_schema.json | 25 ++++-- subworkflows/local/consensus/main.nf | 130 ++++++++++++++------------- workflows/preprocessing.nf | 65 ++++++-------- 5 files changed, 124 insertions(+), 107 deletions(-) diff --git a/modules.json b/modules.json index fc90200c..9c8edb11 100644 --- a/modules.json +++ b/modules.json @@ -148,6 +148,11 @@ "installed_by": ["modules"], "patch": "modules/nf-core/samtools/coverage/samtools-coverage.diff" }, + "samtools/fastq": { + "branch": "master", + "git_sha": "41dfa3f7c0ffabb96a6a813fe321c6d1cc5b6e46", + "installed_by": ["modules"] + }, "samtools/flagstat": { "branch": "master", "git_sha": "2d20463181b1c38981a02e90d3084b5f9fa8d540", @@ -163,6 +168,11 @@ "git_sha": "2d20463181b1c38981a02e90d3084b5f9fa8d540", "installed_by": ["modules"] }, + "samtools/index": { + "branch": "master", + "git_sha": "41dfa3f7c0ffabb96a6a813fe321c6d1cc5b6e46", + "installed_by": ["modules"] + }, "samtools/sormadup": { "branch": "master", "git_sha": "38f3b0200093498b70ac2d63a83eac5642e3c873", diff --git a/nextflow.config b/nextflow.config index b57269c2..885e5f5a 100644 --- a/nextflow.config +++ b/nextflow.config @@ -29,7 +29,6 @@ params { // --- UMI options --- enable_umi = false // turn UMI subworkflow on/off umi_in_readname = true // true: UMI in read name; false: in sequence (future) - enable_duplex = false // run duplex branch (zipper -> DCS) // References diff --git a/nextflow_schema.json b/nextflow_schema.json index 6a2a3685..11807be9 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -61,7 +61,6 @@ }, "umi_aware": { "type": "boolean", - "default": "false", "description": "Run markdup in UMI-aware mode. This applies to Samtools only and requires the UMI to be in the read name." }, "run_coverage": { @@ -71,8 +70,7 @@ }, "skip_trimming": { "type": "boolean", - "description": "Skip adapter trimming", - "default": false + "description": "Skip adapter trimming" }, "trim_front": { "type": "integer", @@ -86,27 +84,22 @@ }, "adapter_R1": { "type": "string", - "default": null, "description": "Adapter sequence to be trimmed" }, "adapter_R2": { "type": "string", - "default": null, "description": "Adapter sequence to be trimmed" }, "disable_picard_metrics": { "type": "boolean", - "default": false, "description": "Disable the calculation of (slow) Picard metrics" }, "roi": { "type": "string", - "default": null, "description": "Region of interest for coverage analysis to be applied to all samples" }, "genelists": { "type": "string", - "default": null, "exists": true, "format": "directory-path", "description": "Directory containing gene list bed files for granular coverage analysis" @@ -266,5 +259,19 @@ { "$ref": "#/$defs/generic_options" } - ] + ], + "properties": { + "umi_in_readname": { + "type": "boolean", + "description": "UMI present in ireadname", + "hidden": true, + "default": true + }, + "enable_umi": { + "type": "boolean", + "description": "Enable UMI subworkflow", + "hidden": true, + "default": true + } + } } diff --git a/subworkflows/local/consensus/main.nf b/subworkflows/local/consensus/main.nf index 6bdbc2a2..8a1170ca 100644 --- a/subworkflows/local/consensus/main.nf +++ b/subworkflows/local/consensus/main.nf @@ -1,73 +1,81 @@ #!/usr/bin/env nextflow -include { FGBIO_COPYUMIFROMREADNAME } from "${projectDir}/modules/nf-core/fgbio/copyumifromreadname/main.nf" -include { FGBIO_CALLDUPLEXCONSENSUSREADS } from "${projectDir}/modules/nf-core/fgbio/callduplexconsensusreads/main.nf" -include { FGBIO_CALLMOLECULARCONSENSUSREADS } from "${projectDir}/modules/nf-core/fgbio/callmolecularconsensusreads/main.nf" -include { FGBIO_COLLECTDUPLEXSEQMETRICS } from "${projectDir}/modules/nf-core/fgbio/collectduplexseqmetrics/main.nf" -include { FGBIO_FASTQTOBAM } from "${projectDir}/modules/nf-core/fgbio/fastqtobam/main.nf" -include { FGBIO_FILTERCONSENSUSREADS } from "${projectDir}/modules/nf-core/fgbio/filterconsensusreads/main.nf" -include { FGBIO_GROUPREADSBYUMI } from "${projectDir}/modules/nf-core/fgbio/groupreadsbyumi/main.nf" -include { FGBIO_SORTBAM } from "${projectDir}/modules/nf-core/fgbio/sortbam/main.nf" -include { FGBIO_ZIPPERBAMS } from "${projectDir}/modules/nf-core/fgbio/zipperbams/main.nf" +include { FGBIO_COPYUMIFROMREADNAME } from '../../../modules/nf-core/fgbio/copyumifromreadname/main' +include { FGBIO_CALLMOLECULARCONSENSUSREADS } from '../../../modules/nf-core/fgbio/callmolecularconsensusreads/main' +include { FGBIO_FASTQTOBAM as FASTQTOBAM_READNAME } from '../../../modules/nf-core/fgbio/fastqtobam/main' +include { FGBIO_FASTQTOBAM as FASTQTOBAM_SEQ } from '../../../modules/nf-core/fgbio/fastqtobam/main' +include { FGBIO_FILTERCONSENSUSREADS } from '../../../modules/nf-core/fgbio/filterconsensusreads/main' +include { FGBIO_GROUPREADSBYUMI } from '../../../modules/nf-core/fgbio/groupreadsbyumi/main' +include { FGBIO_SORTBAM } from '../../../modules/nf-core/fgbio/sortbam/main' +include { FGBIO_ZIPPERBAMS } from '../../../modules/nf-core/fgbio/zipperbams/main' +include { SAMTOOLS_FASTQ } from '../../../modules/nf-core/samtools/fastq/main' +include { SAMTOOLS_INDEX } from '../../../modules/nf-core/samtools/index/main' +include { BWA_MEM } from '../../../modules/nf-core/bwa/mem/main' +include { samplesheetToList } from 'plugin/nf-schema' + workflow CONSENSUS { take: - ch_fastq // channel: tuple(meta, fastq1, fastq2) for SE/PE/duplex samples - ch_reference // channel: reference genome fasta file - ch_umi_in_readname // boolean - + ch_input_fastq // channel: [meta_with_readgroup, fastq] for SE/PE/duplex samples + ch_genomes // map: reference genome files + ch_umi_in_readname // boolean main: + ch_versions = Channel.empty() - ch_duplex_metrics = Channel.empty() - - ch_fastq - .combine(ch_umi_in_readname) - .branch( - rn: { it[3] == true }, - seq: { it[3] == false } - ) - .set { ch_fastq_branch } - - RN_FQ = ch_fastq_branch.rn .map { meta, r1, r2, f -> [meta, r1, r2] } - SEQ_FQ = ch_fastq_branch.seq.map { meta, r1, r2, f -> [meta, r1, r2] } - - FGBIO_FASTQTOBAM(RN_FQ, ch_reference) - ch_versions = ch_versions.mix(FGBIO_FASTQTOBAM.out.versions.first()) - - FGBIO_COPYUMIFROMREADNAME(FGBIO_FASTQTOBAM.out.bam) - ch_versions = ch_versions.mix(FGBIO_COPYUMIFROMREADNAME.out.versions.first()) - - FGBIO_SORTBAM(FGBIO_COPYUMIFROMREADNAME.out.bam) - ch_versions = ch_versions.mix(FGBIO_SORTBAM.out.versions.first()) - - FGBIO_GROUPREADSBYUMI(FGBIO_SORTBAM.out.bam) - ch_versions = ch_versions.mix(FGBIO_GROUPREADSBYUMI.out.versions.first()) - - FGBIO_CALLMOLECULARCONSENSUSREADS(FGBIO_GROUPREADSBYUMI.out.bam) - ch_versions = ch_versions.mix(FGBIO_CALLMOLECULARCONSENSUSREADS.out.versions.first()) - - if (params.enable_duplex) { - FGBIO_ZIPPERBAMS(FGBIO_CALLMOLECULARCONSENSUSREADS.out.bam) - ch_versions = ch_versions.mix(FGBIO_ZIPPERBAMS.out.versions.first()) - FGBIO_CALLDUPLEXCONSENSUSREADS(FGBIO_ZIPPERBAMS.out.bam) - ch_versions = ch_versions.mix(FGBIO_CALLDUPLEXCONSENSUSREADS.out.versions.first()) - FGBIO_FILTERCONSENSUSREADS(FGBIO_CALLDUPLEXCONSENSUSREADS.out.bam) - ch_versions = ch_versions.mix(FGBIO_FILTERCONSENSUSREADS.out.versions.first()) - FGBIO_COLLECTDUPLEXSEQMETRICS(FGBIO_FILTERCONSENSUSREADS.out.bam) - ch_versions = ch_versions.mix(FGBIO_COLLECTDUPLEXSEQMETRICS.out.versions.first()) - ch_duplex_metrics = ch_duplex_metrics.mix(FGBIO_COLLECTDUPLEXSEQMETRICS.out.metrics) - } else { - FGBIO_FILTERCONSENSUSREADS(FGBIO_CALLMOLECULARCONSENSUSREADS.out.bam) - ch_versions = ch_versions.mix(FGBIO_FILTERCONSENSUSREADS.out.versions.first()) - } - - ch_consensus_rn = FGBIO_FILTERCONSENSUSREADS.out.bam - ch_consensus_seq = Channel.empty() // pending: ExtractUmisFromBam branch - - ch_consensus = ch_consensus_rn.mix(ch_consensus_seq) + + ch_input_fastq + .combine(ch_umi_in_readname) + .map { meta, fq1, fq2, umi_flag -> + tuple(meta, fq1, fq2, umi_flag) + } + .branch { _meta, _fq1, _fq2, umi_flag -> + umi_in_readname: umi_flag == true + umi_in_seq: umi_flag == false + } + .set {ch_input_fastq_branch} + + + // Readname branch + + // 1.1: FASTQ => uBAM + ch_RN_uBAM = Channel.empty() + RN_FQ = ch_input_fastq_branch.umi_in_readname.map { meta, r1, r2, _f -> tuple(meta, [r1, r2]) } + + FASTQTOBAM_READNAME(RN_FQ) + SAMTOOLS_INDEX(FASTQTOBAM_READNAME.out.bam) + + FASTQTOBAM_READNAME.out.bam + .join(SAMTOOLS_INDEX.out.bai, by: 0) + .map { meta, bam, bai -> tuple(meta, bam, bai) } + .set { CH_FASTQTOBAM_WITH_BAI } + + FGBIO_COPYUMIFROMREADNAME(CH_FASTQTOBAM_WITH_BAI) + ch_RN_uBAM = ch_RN_uBAM.mix(FGBIO_COPYUMIFROMREADNAME.out.bam) + + // 1.2: uBAM => Mapped BAM + + + + + + + +/* + // Seq branch => uBAM + + + // 1.1: FASTQ => uBAM + SEQ_FQ = ch_input_fastq_branch.umi_in_seq.map { meta, r1, r2, _f -> tuple(meta, [r1, r2]) } + + FASTQTOBAM_SEQ(SEQ_FQ) +*/ emit: + /* consensus_bam = ch_consensus duplex_metrics = ch_duplex_metrics versions = ch_versions -} \ No newline at end of file + */ + consensus_bam = Channel.empty() + versions = Channel.empty() +} diff --git a/workflows/preprocessing.nf b/workflows/preprocessing.nf index d22e4ae5..b5b1a191 100644 --- a/workflows/preprocessing.nf +++ b/workflows/preprocessing.nf @@ -19,6 +19,7 @@ include { BCL_DEMULTIPLEX } from '../subworkflows/nf-core/bcl_demultiplex include { COVERAGE } from '../subworkflows/local/coverage/main' include { FASTQ_TO_UCRAM } from '../subworkflows/local/fastq_to_unaligned_cram/main' include { FASTQ_TO_CRAM } from '../subworkflows/local/fastq_to_aligned_cram/main' + include { CONSENSUS } from '../subworkflows/local/consensus/main' // Functions @@ -134,6 +135,34 @@ workflow PREPROCESSING { } .set {ch_input_fastq} +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// STEP: UMI CONSENSUS (optional, before alignment) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +*/ + +UMI_FLAG = Channel.value(params.umi_in_readname as boolean) +if (params.enable_umi) { + + // Convert to [meta, r1, r2] for the UMI subworkflow + CH_UMI_FASTQ = ch_input_fastq.map { meta, reads -> + def r1 = (reads instanceof List) ? reads[0] : reads + def r2 = (reads instanceof List && reads.size() > 1) ? reads[1] : null + return [meta, r1, r2] + } + + // Call the UMI subworkflow “like a function” + // NOTE: your CONSENSUS expects (ch_fastq, ch_reference, ch_umi_in_readname) + // If your CONSENSUS expects a single fasta (not keyed), replace CH_UMI_FASTA by Channel.fromPath(...). + + CONSENSUS(CH_UMI_FASTQ, genomes, UMI_FLAG) + + ch_versions = ch_versions.mix(CONSENSUS.out.versions) + + // Output channels + ch_umi_consensus_bam = CONSENSUS.out.consensus_bam +} + /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ASSOCIATE CORRECT GENOME AND COUNT SAMPLE REPLICATES @@ -236,42 +265,6 @@ workflow PREPROCESSING { ch_trimmed_reads.other.dump(tag:"Other trimmed reads per sample", pretty: true) -/* -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -// STEP: UMI CONSENSUS (optional, before alignment) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -*/ - -UMI_FLAG = Channel.value(params.umi_in_readname as boolean) - -if (params.enable_umi) { - - // ch_trimmed_reads.supported: [meta, reads] - // Convert to [meta, r1, r2] for the UMI subworkflow - CH_UMI_FASTQ = ch_trimmed_reads.supported.map { meta, reads -> - def r1 = (reads instanceof List) ? reads[0] : reads - def r2 = (reads instanceof List && reads.size() > 1) ? reads[1] : null - return [meta, r1, r2] - } - - // Per-sample reference fasta (same that you use later for alignment) - CH_UMI_FASTA = ch_trimmed_reads.supported.map { meta, reads -> - return [meta, getGenomeAttribute(meta.genome_data, "fasta")] - } - - // Call the UMI subworkflow “like a function” - // NOTE: your CONSENSUS expects (ch_fastq, ch_reference, ch_umi_in_readname) - // If your CONSENSUS expects a single fasta (not keyed), replace CH_UMI_FASTA by Channel.fromPath(...). - CONSENSUS(CH_UMI_FASTQ, CH_UMI_FASTA, UMI_FLAG) - - ch_versions = ch_versions.mix(CONSENSUS.out.versions) - - // Output channels - ch_umi_consensus_bam = CONSENSUS.out.consensus_bam - ch_umi_duplex_metrics = CONSENSUS.out.duplex_metrics -} - - /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // STEP: FASTQ TO UNALIGNED CRAM CONVERSION From a958e106a8492775269d4035151321c45601225e Mon Sep 17 00:00:00 2001 From: hwjeong Date: Tue, 19 Aug 2025 14:58:14 +0200 Subject: [PATCH 05/33] module update --- .../nf-core/samtools/fastq/environment.yml | 8 + modules/nf-core/samtools/fastq/main.nf | 60 ++++ modules/nf-core/samtools/fastq/meta.yml | 96 ++++++ .../nf-core/samtools/fastq/tests/main.nf.test | 119 ++++++++ .../samtools/fastq/tests/main.nf.test.snap | 287 ++++++++++++++++++ .../nf-core/samtools/index/environment.yml | 8 + modules/nf-core/samtools/index/main.nf | 49 +++ modules/nf-core/samtools/index/meta.yml | 77 +++++ .../samtools/index/tests/csi.nextflow.config | 7 + .../nf-core/samtools/index/tests/main.nf.test | 140 +++++++++ .../samtools/index/tests/main.nf.test.snap | 250 +++++++++++++++ 11 files changed, 1101 insertions(+) create mode 100644 modules/nf-core/samtools/fastq/environment.yml create mode 100644 modules/nf-core/samtools/fastq/main.nf create mode 100644 modules/nf-core/samtools/fastq/meta.yml create mode 100644 modules/nf-core/samtools/fastq/tests/main.nf.test create mode 100644 modules/nf-core/samtools/fastq/tests/main.nf.test.snap create mode 100644 modules/nf-core/samtools/index/environment.yml create mode 100644 modules/nf-core/samtools/index/main.nf create mode 100644 modules/nf-core/samtools/index/meta.yml create mode 100644 modules/nf-core/samtools/index/tests/csi.nextflow.config create mode 100644 modules/nf-core/samtools/index/tests/main.nf.test create mode 100644 modules/nf-core/samtools/index/tests/main.nf.test.snap diff --git a/modules/nf-core/samtools/fastq/environment.yml b/modules/nf-core/samtools/fastq/environment.yml new file mode 100644 index 00000000..62054fc9 --- /dev/null +++ b/modules/nf-core/samtools/fastq/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::htslib=1.21 + - bioconda::samtools=1.21 diff --git a/modules/nf-core/samtools/fastq/main.nf b/modules/nf-core/samtools/fastq/main.nf new file mode 100644 index 00000000..bcc5d604 --- /dev/null +++ b/modules/nf-core/samtools/fastq/main.nf @@ -0,0 +1,60 @@ +process SAMTOOLS_FASTQ { + tag "$meta.id" + label 'process_low' + + conda "${moduleDir}/environment.yml" + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'https://depot.galaxyproject.org/singularity/samtools:1.21--h50ea8bc_0' : + 'biocontainers/samtools:1.21--h50ea8bc_0' }" + + input: + tuple val(meta), path(input) + val(interleave) + + output: + tuple val(meta), path("*_{1,2}.fastq.gz") , optional:true, emit: fastq + tuple val(meta), path("*_interleaved.fastq") , optional:true, emit: interleaved + tuple val(meta), path("*_singleton.fastq.gz") , optional:true, emit: singleton + tuple val(meta), path("*_other.fastq.gz") , optional:true, emit: other + 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}" + def output = ( interleave && ! meta.single_end ) ? "> ${prefix}_interleaved.fastq" : + meta.single_end ? "-1 ${prefix}_1.fastq.gz -s ${prefix}_singleton.fastq.gz" : + "-1 ${prefix}_1.fastq.gz -2 ${prefix}_2.fastq.gz -s ${prefix}_singleton.fastq.gz" + """ + # Note: --threads value represents *additional* CPUs to allocate (total CPUs = 1 + --threads). + samtools \\ + fastq \\ + $args \\ + --threads ${task.cpus-1} \\ + -0 ${prefix}_other.fastq.gz \\ + $input \\ + $output + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + samtools: \$(echo \$(samtools --version 2>&1) | sed 's/^.*samtools //; s/Using.*\$//') + END_VERSIONS + """ + + stub: + def prefix = task.ext.prefix ?: "${meta.id}" + def output = ( interleave && ! meta.single_end ) ? "touch ${prefix}_interleaved.fastq" : + meta.single_end ? "echo | gzip > ${prefix}_1.fastq.gz && echo | gzip > ${prefix}_singleton.fastq.gz" : + "echo | gzip > ${prefix}_1.fastq.gz && echo | gzip > ${prefix}_2.fastq.gz && echo | gzip > ${prefix}_singleton.fastq.gz" + """ + ${output} + echo | gzip > ${prefix}_other.fastq.gz + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + samtools: \$(echo \$(samtools --version 2>&1) | sed 's/^.*samtools //; s/Using.*\$//') + END_VERSIONS + """ +} diff --git a/modules/nf-core/samtools/fastq/meta.yml b/modules/nf-core/samtools/fastq/meta.yml new file mode 100644 index 00000000..9a5bd42f --- /dev/null +++ b/modules/nf-core/samtools/fastq/meta.yml @@ -0,0 +1,96 @@ +name: samtools_fastq +description: Converts a SAM/BAM/CRAM file to FASTQ +keywords: + - bam + - sam + - cram + - fastq +tools: + - samtools: + description: | + SAMtools is a set of utilities for interacting with and post-processing + short DNA sequence read alignments in the SAM, BAM and CRAM formats, written by Heng Li. + These files are generated as output by short read aligners like BWA. + homepage: http://www.htslib.org/ + documentation: http://www.htslib.org/doc/samtools.html + doi: 10.1093/bioinformatics/btp352 + licence: ["MIT"] + identifier: biotools:samtools +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - input: + type: file + description: BAM/CRAM/SAM file + pattern: "*.{bam,cram,sam}" + ontologies: [] + - interleave: + type: boolean + description: Set true for interleaved fastq file +output: + fastq: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - "*_{1,2}.fastq.gz": + type: file + description: Compressed FASTQ file(s) with reads with either the READ1 or + READ2 flag set in separate files. + pattern: "*_{1,2}.fastq.gz" + ontologies: [] + interleaved: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - "*_interleaved.fastq": + type: file + description: Compressed FASTQ file with reads with either the READ1 or READ2 + flag set in a combined file. Needs collated input file. + pattern: "*_interleaved.fastq.gz" + ontologies: + - edam: http://edamontology.org/format_3989 # GZIP format + singleton: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - "*_singleton.fastq.gz": + type: file + description: Compressed FASTQ file with singleton reads + pattern: "*_singleton.fastq.gz" + ontologies: + - edam: http://edamontology.org/format_3989 # GZIP format + other: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - "*_other.fastq.gz": + type: file + description: Compressed FASTQ file with reads with either both READ1 and READ2 + flags set or unset + pattern: "*_other.fastq.gz" + ontologies: + - edam: http://edamontology.org/format_3989 # GZIP format + versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" + ontologies: + - edam: http://edamontology.org/format_3750 # YAML +authors: + - "@priyanka-surana" + - "@suzannejin" +maintainers: + - "@priyanka-surana" + - "@suzannejin" diff --git a/modules/nf-core/samtools/fastq/tests/main.nf.test b/modules/nf-core/samtools/fastq/tests/main.nf.test new file mode 100644 index 00000000..971ea1d4 --- /dev/null +++ b/modules/nf-core/samtools/fastq/tests/main.nf.test @@ -0,0 +1,119 @@ +nextflow_process { + + name "Test Process SAMTOOLS_FASTQ" + script "../main.nf" + process "SAMTOOLS_FASTQ" + + tag "modules" + tag "modules_nfcore" + tag "samtools" + tag "samtools/fastq" + + test("bam") { + + when { + process { + """ + interleave = false + + input[0] = Channel.of([ + [ id:'test', single_end:false ], // meta map + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/bam/test.paired_end.bam', checkIfExists: true) + ]) + input[1] = interleave + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out.fastq[0][1].collect { path(it).linesGzip[0..6] }).match("bam_fastq") }, + { assert snapshot(process.out.interleaved).match("bam_interleaved") }, + { assert snapshot(file(process.out.singleton[0][1]).name).match("bam_singleton") }, + { assert snapshot(file(process.out.other[0][1]).name).match("bam_other") }, + { assert snapshot(process.out.versions).match("bam_versions") } + ) + } + } + + test("bam_interleave") { + + when { + process { + """ + interleave = true + + input[0] = Channel.of([ + [ id:'test', single_end:false ], // meta map + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/bam/test.paired_end.bam', checkIfExists: true) + ]) + input[1] = interleave + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out.fastq).match("bam_interleave_fastq") }, + { assert snapshot(path(process.out.interleaved[0][1]).readLines()[0..6]).match("bam_interlinterleave_eaved") }, + { assert snapshot(process.out.singleton).match("bam_singinterleave_leton") }, + { assert snapshot(file(process.out.other[0][1]).name).match("bam_interleave_other") }, + { assert snapshot(process.out.versions).match("bam_verinterleave_sions") } + ) + } + } + + test("bam - stub") { + + options "-stub" + + when { + process { + """ + interleave = false + + input[0] = Channel.of([ + [ id:'test', single_end:false ], // meta map + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/bam/test.paired_end.bam', checkIfExists: true) + ]) + input[1] = interleave + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } + + test("bam_interleave - stub") { + + options "-stub" + + when { + process { + """ + interleave = true + + input[0] = Channel.of([ + [ id:'test', single_end:false ], // meta map + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/bam/test.paired_end.bam', checkIfExists: true) + ]) + input[1] = interleave + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } +} diff --git a/modules/nf-core/samtools/fastq/tests/main.nf.test.snap b/modules/nf-core/samtools/fastq/tests/main.nf.test.snap new file mode 100644 index 00000000..ff63f9ae --- /dev/null +++ b/modules/nf-core/samtools/fastq/tests/main.nf.test.snap @@ -0,0 +1,287 @@ +{ + "bam_interlinterleave_eaved": { + "content": [ + [ + "@ERR5069949.2151832/1", + "TCATAAACCAAAGCACTCACAGTGTCAACAATTTCAGCAGGACAACGCCGACAAGTTCCGAGGAACATGTCTGGACCTATAGTTTTCATAAGTCTACACACTGAATTGAAATATTCTGGTTCTAGTGTGCCCTTAGTTAGCAATGTGCGT", + "+", + "AAAAAAEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEAAEEEEAEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEAAEEEEE versions.yml + "${task.process}": + samtools: \$(echo \$(samtools --version 2>&1) | sed 's/^.*samtools //; s/Using.*\$//') + END_VERSIONS + """ + + stub: + def args = task.ext.args ?: '' + def extension = file(input).getExtension() == 'cram' ? + "crai" : args.contains("-c") ? "csi" : "bai" + """ + touch ${input}.${extension} + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + samtools: \$(echo \$(samtools --version 2>&1) | sed 's/^.*samtools //; s/Using.*\$//') + END_VERSIONS + """ +} diff --git a/modules/nf-core/samtools/index/meta.yml b/modules/nf-core/samtools/index/meta.yml new file mode 100644 index 00000000..1bed6bca --- /dev/null +++ b/modules/nf-core/samtools/index/meta.yml @@ -0,0 +1,77 @@ +name: samtools_index +description: Index SAM/BAM/CRAM file +keywords: + - index + - bam + - sam + - cram +tools: + - samtools: + description: | + SAMtools is a set of utilities for interacting with and post-processing + short DNA sequence read alignments in the SAM, BAM and CRAM formats, written by Heng Li. + These files are generated as output by short read aligners like BWA. + homepage: http://www.htslib.org/ + documentation: http://www.htslib.org/doc/samtools.html + doi: 10.1093/bioinformatics/btp352 + licence: ["MIT"] + identifier: biotools:samtools +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - input: + type: file + description: input file + ontologies: [] +output: + bai: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - "*.bai": + type: file + description: BAM/CRAM/SAM index file + pattern: "*.{bai,crai,sai}" + ontologies: [] + csi: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - "*.csi": + type: file + description: CSI index file + pattern: "*.{csi}" + ontologies: [] + crai: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - "*.crai": + type: file + description: BAM/CRAM/SAM index file + pattern: "*.{bai,crai,sai}" + ontologies: [] + versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" + ontologies: + - edam: http://edamontology.org/format_3750 # YAML +authors: + - "@drpatelh" + - "@ewels" + - "@maxulysse" +maintainers: + - "@drpatelh" + - "@ewels" + - "@maxulysse" diff --git a/modules/nf-core/samtools/index/tests/csi.nextflow.config b/modules/nf-core/samtools/index/tests/csi.nextflow.config new file mode 100644 index 00000000..0ed260ef --- /dev/null +++ b/modules/nf-core/samtools/index/tests/csi.nextflow.config @@ -0,0 +1,7 @@ +process { + + withName: SAMTOOLS_INDEX { + ext.args = '-c' + } + +} diff --git a/modules/nf-core/samtools/index/tests/main.nf.test b/modules/nf-core/samtools/index/tests/main.nf.test new file mode 100644 index 00000000..ca34fb5c --- /dev/null +++ b/modules/nf-core/samtools/index/tests/main.nf.test @@ -0,0 +1,140 @@ +nextflow_process { + + name "Test Process SAMTOOLS_INDEX" + script "../main.nf" + process "SAMTOOLS_INDEX" + tag "modules" + tag "modules_nfcore" + tag "samtools" + tag "samtools/index" + + test("bai") { + when { + process { + """ + input[0] = Channel.of([ + [ id:'test', single_end:false ], // meta map + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/bam/test.paired_end.sorted.bam', checkIfExists: true) + ]) + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } + + test("crai") { + when { + process { + """ + input[0] = Channel.of([ + [ id:'test', single_end:false ], // meta map + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/cram/test.paired_end.recalibrated.sorted.cram', checkIfExists: true) + ]) + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } + + test("csi") { + config "./csi.nextflow.config" + + when { + process { + """ + input[0] = Channel.of([ + [ id:'test', single_end:false ], // meta map + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/bam/test.paired_end.sorted.bam', checkIfExists: true) + ]) + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot( + file(process.out.csi[0][1]).name, + process.out.versions + ).match() } + ) + } + } + + test("bai - stub") { + options "-stub" + when { + process { + """ + input[0] = Channel.of([ + [ id:'test', single_end:false ], // meta map + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/bam/test.paired_end.sorted.bam', checkIfExists: true) + ]) + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } + + test("crai - stub") { + options "-stub" + when { + process { + """ + input[0] = Channel.of([ + [ id:'test', single_end:false ], // meta map + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/cram/test.paired_end.recalibrated.sorted.cram', checkIfExists: true) + ]) + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } + + test("csi - stub") { + options "-stub" + config "./csi.nextflow.config" + + when { + process { + """ + input[0] = Channel.of([ + [ id:'test', single_end:false ], // meta map + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/bam/test.paired_end.sorted.bam', checkIfExists: true) + ]) + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } +} diff --git a/modules/nf-core/samtools/index/tests/main.nf.test.snap b/modules/nf-core/samtools/index/tests/main.nf.test.snap new file mode 100644 index 00000000..72d65e81 --- /dev/null +++ b/modules/nf-core/samtools/index/tests/main.nf.test.snap @@ -0,0 +1,250 @@ +{ + "csi - stub": { + "content": [ + { + "0": [ + + ], + "1": [ + [ + { + "id": "test", + "single_end": false + }, + "test.paired_end.sorted.bam.csi:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "2": [ + + ], + "3": [ + "versions.yml:md5,5e09a6fdf76de396728f877193d72315" + ], + "bai": [ + + ], + "crai": [ + + ], + "csi": [ + [ + { + "id": "test", + "single_end": false + }, + "test.paired_end.sorted.bam.csi:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "versions": [ + "versions.yml:md5,5e09a6fdf76de396728f877193d72315" + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-09-16T08:21:25.261127166" + }, + "crai - stub": { + "content": [ + { + "0": [ + + ], + "1": [ + + ], + "2": [ + [ + { + "id": "test", + "single_end": false + }, + "test.paired_end.recalibrated.sorted.cram.crai:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "3": [ + "versions.yml:md5,5e09a6fdf76de396728f877193d72315" + ], + "bai": [ + + ], + "crai": [ + [ + { + "id": "test", + "single_end": false + }, + "test.paired_end.recalibrated.sorted.cram.crai:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "csi": [ + + ], + "versions": [ + "versions.yml:md5,5e09a6fdf76de396728f877193d72315" + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-09-16T08:21:12.653194876" + }, + "bai - stub": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test.paired_end.sorted.bam.bai:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "1": [ + + ], + "2": [ + + ], + "3": [ + "versions.yml:md5,5e09a6fdf76de396728f877193d72315" + ], + "bai": [ + [ + { + "id": "test", + "single_end": false + }, + "test.paired_end.sorted.bam.bai:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "crai": [ + + ], + "csi": [ + + ], + "versions": [ + "versions.yml:md5,5e09a6fdf76de396728f877193d72315" + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-09-16T08:21:01.854932651" + }, + "csi": { + "content": [ + "test.paired_end.sorted.bam.csi", + [ + "versions.yml:md5,5e09a6fdf76de396728f877193d72315" + ] + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-09-16T08:20:51.485364222" + }, + "crai": { + "content": [ + { + "0": [ + + ], + "1": [ + + ], + "2": [ + [ + { + "id": "test", + "single_end": false + }, + "test.paired_end.recalibrated.sorted.cram.crai:md5,14bc3bd5c89cacc8f4541f9062429029" + ] + ], + "3": [ + "versions.yml:md5,5e09a6fdf76de396728f877193d72315" + ], + "bai": [ + + ], + "crai": [ + [ + { + "id": "test", + "single_end": false + }, + "test.paired_end.recalibrated.sorted.cram.crai:md5,14bc3bd5c89cacc8f4541f9062429029" + ] + ], + "csi": [ + + ], + "versions": [ + "versions.yml:md5,5e09a6fdf76de396728f877193d72315" + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-09-16T08:20:40.518873972" + }, + "bai": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "single_end": false + }, + "test.paired_end.sorted.bam.bai:md5,704c10dd1326482448ca3073fdebc2f4" + ] + ], + "1": [ + + ], + "2": [ + + ], + "3": [ + "versions.yml:md5,5e09a6fdf76de396728f877193d72315" + ], + "bai": [ + [ + { + "id": "test", + "single_end": false + }, + "test.paired_end.sorted.bam.bai:md5,704c10dd1326482448ca3073fdebc2f4" + ] + ], + "crai": [ + + ], + "csi": [ + + ], + "versions": [ + "versions.yml:md5,5e09a6fdf76de396728f877193d72315" + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "24.04.4" + }, + "timestamp": "2024-09-16T08:20:21.184050361" + } +} \ No newline at end of file From 6cb4bb19df911c488c33f5e146e5e096df487255 Mon Sep 17 00:00:00 2001 From: hwjeong Date: Tue, 19 Aug 2025 15:49:54 +0200 Subject: [PATCH 06/33] correction based on feedback --- nextflow_schema.json | 28 ++++++------- subworkflows/local/consensus/main.nf | 63 +++++++++++++++++----------- workflows/preprocessing.nf | 8 ++-- 3 files changed, 55 insertions(+), 44 deletions(-) diff --git a/nextflow_schema.json b/nextflow_schema.json index 11807be9..051a17a3 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -47,6 +47,18 @@ "description": "", "default": "", "properties": { + "enable_umi": { + "type": "boolean", + "description": "Enable UMI subworkflow", + "hidden": true, + "default": true + }, + "umi_in_readname": { + "type": "boolean", + "description": "UMI present in ireadname", + "hidden": true, + "default": true + }, "aligner": { "type": "string", "default": "bowtie2", @@ -259,19 +271,5 @@ { "$ref": "#/$defs/generic_options" } - ], - "properties": { - "umi_in_readname": { - "type": "boolean", - "description": "UMI present in ireadname", - "hidden": true, - "default": true - }, - "enable_umi": { - "type": "boolean", - "description": "Enable UMI subworkflow", - "hidden": true, - "default": true - } - } + ] } diff --git a/subworkflows/local/consensus/main.nf b/subworkflows/local/consensus/main.nf index 8a1170ca..851c37dc 100644 --- a/subworkflows/local/consensus/main.nf +++ b/subworkflows/local/consensus/main.nf @@ -11,63 +11,76 @@ include { FGBIO_ZIPPERBAMS } from '../../../modules/nf-core/fgb include { SAMTOOLS_FASTQ } from '../../../modules/nf-core/samtools/fastq/main' include { SAMTOOLS_INDEX } from '../../../modules/nf-core/samtools/index/main' include { BWA_MEM } from '../../../modules/nf-core/bwa/mem/main' -include { samplesheetToList } from 'plugin/nf-schema' workflow CONSENSUS { take: ch_input_fastq // channel: [meta_with_readgroup, fastq] for SE/PE/duplex samples ch_genomes // map: reference genome files - ch_umi_in_readname // boolean - main: - ch_versions = Channel.empty() + main: + def ch_versions = Channel.empty() + def readname_fastq = Channel.empty() + def ch_readname_uBAM = Channel.empty() + def ch_fastqtobam_with_bai = Channel.empty() ch_input_fastq - .combine(ch_umi_in_readname) + .combine(params.umi_in_readname) .map { meta, fq1, fq2, umi_flag -> tuple(meta, fq1, fq2, umi_flag) } - .branch { _meta, _fq1, _fq2, umi_flag -> - umi_in_readname: umi_flag == true - umi_in_seq: umi_flag == false - } - .set {ch_input_fastq_branch} + .set {ch_input_fastq_combined} - // Readname branch + if (params.umi_in_readname) { - // 1.1: FASTQ => uBAM - ch_RN_uBAM = Channel.empty() - RN_FQ = ch_input_fastq_branch.umi_in_readname.map { meta, r1, r2, _f -> tuple(meta, [r1, r2]) } + // Case 1: UMI_in_readname - FASTQTOBAM_READNAME(RN_FQ) - SAMTOOLS_INDEX(FASTQTOBAM_READNAME.out.bam) + ch_input_fastq_combined + .filter { _meta, _fq1, _fq2, umi_flag -> umi_flag == true } + .set { ch_input_fastq_umi_in_readname } - FASTQTOBAM_READNAME.out.bam - .join(SAMTOOLS_INDEX.out.bai, by: 0) - .map { meta, bam, bai -> tuple(meta, bam, bai) } - .set { CH_FASTQTOBAM_WITH_BAI } - FGBIO_COPYUMIFROMREADNAME(CH_FASTQTOBAM_WITH_BAI) - ch_RN_uBAM = ch_RN_uBAM.mix(FGBIO_COPYUMIFROMREADNAME.out.bam) + // 1.1: FASTQ => uBAM - // 1.2: uBAM => Mapped BAM + readname_fastq = ch_input_fastq_umi_in_readname.map { meta, r1, r2, _f -> tuple(meta, [r1, r2]) } + FASTQTOBAM_READNAME(readname_fastq) + ch_versions = ch_versions.mix(FASTQTOBAM_READNAME.out.versions) + SAMTOOLS_INDEX(FASTQTOBAM_READNAME.out.bam) + ch_versions = ch_versions.mix(SAMTOOLS_INDEX.out.versions) + FASTQTOBAM_READNAME.out.bam + .join(SAMTOOLS_INDEX.out.bai, by: 0) + .map { meta, bam, bai -> tuple(meta, bam, bai) } + .set { ch_fastqtobam_with_bai } + FGBIO_COPYUMIFROMREADNAME(ch_fastqtobam_with_bai) + ch_versions = ch_versions.mix(FGBIO_COPYUMIFROMREADNAME.out.versions) + ch_readname_uBAM = ch_readname_uBAM.mix(FGBIO_COPYUMIFROMREADNAME.out.bam) + // 1.2: uBAM => Mapped BAM + } else { + + // Case 2: UMI_in_sequence + + ch_input_fastq_combined + .filter { _meta, _fq1, _fq2, umi_flag -> umi_flag == false } + .set { ch_input_fastq_umi_in_seq } + } /* - // Seq branch => uBAM + // Seq branch => uBAM // 1.1: FASTQ => uBAM SEQ_FQ = ch_input_fastq_branch.umi_in_seq.map { meta, r1, r2, _f -> tuple(meta, [r1, r2]) } FASTQTOBAM_SEQ(SEQ_FQ) + + // 1.2: uBAM -> Mapped BAM */ emit: @@ -77,5 +90,5 @@ workflow CONSENSUS { versions = ch_versions */ consensus_bam = Channel.empty() - versions = Channel.empty() + versions = ch_versions } diff --git a/workflows/preprocessing.nf b/workflows/preprocessing.nf index b5b1a191..e6b29133 100644 --- a/workflows/preprocessing.nf +++ b/workflows/preprocessing.nf @@ -141,13 +141,13 @@ workflow PREPROCESSING { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ -UMI_FLAG = Channel.value(params.umi_in_readname as boolean) + if (params.enable_umi) { // Convert to [meta, r1, r2] for the UMI subworkflow - CH_UMI_FASTQ = ch_input_fastq.map { meta, reads -> + ch_umi_fastq = ch_input_fastq.map { meta, reads -> def r1 = (reads instanceof List) ? reads[0] : reads - def r2 = (reads instanceof List && reads.size() > 1) ? reads[1] : null + def r2 = (reads instanceof List && reads.size() > 1) ? reads[1] : [] return [meta, r1, r2] } @@ -155,7 +155,7 @@ if (params.enable_umi) { // NOTE: your CONSENSUS expects (ch_fastq, ch_reference, ch_umi_in_readname) // If your CONSENSUS expects a single fasta (not keyed), replace CH_UMI_FASTA by Channel.fromPath(...). - CONSENSUS(CH_UMI_FASTQ, genomes, UMI_FLAG) + CONSENSUS(ch_umi_fastq, genomes) ch_versions = ch_versions.mix(CONSENSUS.out.versions) From 34809ea117baace15c145d226d87510579ade12d Mon Sep 17 00:00:00 2001 From: hwjeong Date: Tue, 19 Aug 2025 16:19:10 +0200 Subject: [PATCH 07/33] 2nd correction --- subworkflows/local/consensus/main.nf | 34 +++++++++++----------------- workflows/preprocessing.nf | 4 ++-- 2 files changed, 15 insertions(+), 23 deletions(-) diff --git a/subworkflows/local/consensus/main.nf b/subworkflows/local/consensus/main.nf index 851c37dc..55dc98a4 100644 --- a/subworkflows/local/consensus/main.nf +++ b/subworkflows/local/consensus/main.nf @@ -21,29 +21,18 @@ workflow CONSENSUS { main: def ch_versions = Channel.empty() def readname_fastq = Channel.empty() - def ch_readname_uBAM = Channel.empty() + def seq_fastq = Channel.empty() + def ch_fastq_to_uBAM = Channel.empty() def ch_fastqtobam_with_bai = Channel.empty() - ch_input_fastq - .combine(params.umi_in_readname) - .map { meta, fq1, fq2, umi_flag -> - tuple(meta, fq1, fq2, umi_flag) - } - .set {ch_input_fastq_combined} - + // 1.1: FASTQ => uBAM if (params.umi_in_readname) { // Case 1: UMI_in_readname - ch_input_fastq_combined - .filter { _meta, _fq1, _fq2, umi_flag -> umi_flag == true } - .set { ch_input_fastq_umi_in_readname } - - // 1.1: FASTQ => uBAM - - readname_fastq = ch_input_fastq_umi_in_readname.map { meta, r1, r2, _f -> tuple(meta, [r1, r2]) } + readname_fastq = ch_input_fastq.map { meta, r1, r2 -> tuple(meta, [r1, r2]) } FASTQTOBAM_READNAME(readname_fastq) ch_versions = ch_versions.mix(FASTQTOBAM_READNAME.out.versions) @@ -59,18 +48,21 @@ workflow CONSENSUS { FGBIO_COPYUMIFROMREADNAME(ch_fastqtobam_with_bai) ch_versions = ch_versions.mix(FGBIO_COPYUMIFROMREADNAME.out.versions) - ch_readname_uBAM = ch_readname_uBAM.mix(FGBIO_COPYUMIFROMREADNAME.out.bam) - - // 1.2: uBAM => Mapped BAM + ch_fastq_to_uBAM = ch_fastq_to_uBAM.mix(FGBIO_COPYUMIFROMREADNAME.out.bam) } else { // Case 2: UMI_in_sequence - ch_input_fastq_combined - .filter { _meta, _fq1, _fq2, umi_flag -> umi_flag == false } - .set { ch_input_fastq_umi_in_seq } + seq_fastq = ch_input_fastq.map { meta, r1, r2 -> tuple(meta, [r1, r2]) } + + FASTQTOBAM_SEQ(seq_fastq) + ch_versions = ch_versions.mix(FASTQTOBAM_SEQ.out.versions) + + ch_fastq_to_uBAM = ch_fastq_to_uBAM.mix(FASTQTOBAM_SEQ.out.bam) } + + // 1.2: uBAM => Mapped BAM /* // Seq branch => uBAM diff --git a/workflows/preprocessing.nf b/workflows/preprocessing.nf index e6b29133..55a270f1 100644 --- a/workflows/preprocessing.nf +++ b/workflows/preprocessing.nf @@ -145,7 +145,7 @@ workflow PREPROCESSING { if (params.enable_umi) { // Convert to [meta, r1, r2] for the UMI subworkflow - ch_umi_fastq = ch_input_fastq.map { meta, reads -> + def ch_umi_fastq = ch_input_fastq.map { meta, reads -> def r1 = (reads instanceof List) ? reads[0] : reads def r2 = (reads instanceof List && reads.size() > 1) ? reads[1] : [] return [meta, r1, r2] @@ -160,7 +160,7 @@ if (params.enable_umi) { ch_versions = ch_versions.mix(CONSENSUS.out.versions) // Output channels - ch_umi_consensus_bam = CONSENSUS.out.consensus_bam + def ch_umi_consensus_bam = CONSENSUS.out.consensus_bam } /* From 817691198ef4096e3edc81d1c21e9ec52eeda424 Mon Sep 17 00:00:00 2001 From: Dhamar Date: Wed, 20 Aug 2025 11:06:44 +0200 Subject: [PATCH 08/33] Step 1.2 ubam => Mapped BAM --- subworkflows/local/consensus/main.nf | 74 ++++++++++++++++------------ workflows/preprocessing.nf | 7 ++- 2 files changed, 47 insertions(+), 34 deletions(-) diff --git a/subworkflows/local/consensus/main.nf b/subworkflows/local/consensus/main.nf index 55dc98a4..2a149692 100644 --- a/subworkflows/local/consensus/main.nf +++ b/subworkflows/local/consensus/main.nf @@ -1,16 +1,16 @@ #!/usr/bin/env nextflow -include { FGBIO_COPYUMIFROMREADNAME } from '../../../modules/nf-core/fgbio/copyumifromreadname/main' -include { FGBIO_CALLMOLECULARCONSENSUSREADS } from '../../../modules/nf-core/fgbio/callmolecularconsensusreads/main' -include { FGBIO_FASTQTOBAM as FASTQTOBAM_READNAME } from '../../../modules/nf-core/fgbio/fastqtobam/main' -include { FGBIO_FASTQTOBAM as FASTQTOBAM_SEQ } from '../../../modules/nf-core/fgbio/fastqtobam/main' -include { FGBIO_FILTERCONSENSUSREADS } from '../../../modules/nf-core/fgbio/filterconsensusreads/main' -include { FGBIO_GROUPREADSBYUMI } from '../../../modules/nf-core/fgbio/groupreadsbyumi/main' -include { FGBIO_SORTBAM } from '../../../modules/nf-core/fgbio/sortbam/main' -include { FGBIO_ZIPPERBAMS } from '../../../modules/nf-core/fgbio/zipperbams/main' -include { SAMTOOLS_FASTQ } from '../../../modules/nf-core/samtools/fastq/main' -include { SAMTOOLS_INDEX } from '../../../modules/nf-core/samtools/index/main' -include { BWA_MEM } from '../../../modules/nf-core/bwa/mem/main' +include { FGBIO_COPYUMIFROMREADNAME } from '../../../modules/nf-core/fgbio/copyumifromreadname/main' +include { FGBIO_CALLMOLECULARCONSENSUSREADS } from '../../../modules/nf-core/fgbio/callmolecularconsensusreads/main' +include { FGBIO_FASTQTOBAM as FASTQTOBAM_READNAME } from '../../../modules/nf-core/fgbio/fastqtobam/main' +include { FGBIO_FASTQTOBAM as FASTQTOBAM_SEQ } from '../../../modules/nf-core/fgbio/fastqtobam/main' +include { FGBIO_FILTERCONSENSUSREADS } from '../../../modules/nf-core/fgbio/filterconsensusreads/main' +include { FGBIO_GROUPREADSBYUMI } from '../../../modules/nf-core/fgbio/groupreadsbyumi/main' +include { FGBIO_SORTBAM } from '../../../modules/nf-core/fgbio/sortbam/main' +include { FGBIO_ZIPPERBAMS } from '../../../modules/nf-core/fgbio/zipperbams/main' +include { SAMTOOLS_FASTQ } from '../../../modules/nf-core/samtools/fastq/main' +include { SAMTOOLS_INDEX } from '../../../modules/nf-core/samtools/index/main' +include { BWA_MEM } from '../../../modules/nf-core/bwa/mem/main' workflow CONSENSUS { @@ -19,11 +19,11 @@ workflow CONSENSUS { ch_genomes // map: reference genome files main: - def ch_versions = Channel.empty() - def readname_fastq = Channel.empty() - def seq_fastq = Channel.empty() - def ch_fastq_to_uBAM = Channel.empty() - def ch_fastqtobam_with_bai = Channel.empty() + def ch_versions = Channel.empty() + def ch_fastq_readname = Channel.empty() + def ch_fastq_seq = Channel.empty() + def ch_ubam = Channel.empty() + def ch_ubam_with_bai = Channel.empty() // 1.1: FASTQ => uBAM @@ -32,9 +32,9 @@ workflow CONSENSUS { // Case 1: UMI_in_readname - readname_fastq = ch_input_fastq.map { meta, r1, r2 -> tuple(meta, [r1, r2]) } + ch_fastq_readname = ch_input_fastq.map { meta, r1, r2 -> tuple(meta, [r1, r2]) } - FASTQTOBAM_READNAME(readname_fastq) + FASTQTOBAM_READNAME(ch_fastq_readname) ch_versions = ch_versions.mix(FASTQTOBAM_READNAME.out.versions) SAMTOOLS_INDEX(FASTQTOBAM_READNAME.out.bam) @@ -43,37 +43,47 @@ workflow CONSENSUS { FASTQTOBAM_READNAME.out.bam .join(SAMTOOLS_INDEX.out.bai, by: 0) .map { meta, bam, bai -> tuple(meta, bam, bai) } - .set { ch_fastqtobam_with_bai } + .set { ch_ubam_with_bai } - FGBIO_COPYUMIFROMREADNAME(ch_fastqtobam_with_bai) + FGBIO_COPYUMIFROMREADNAME(ch_ubam_with_bai) ch_versions = ch_versions.mix(FGBIO_COPYUMIFROMREADNAME.out.versions) - ch_fastq_to_uBAM = ch_fastq_to_uBAM.mix(FGBIO_COPYUMIFROMREADNAME.out.bam) + ch_ubam = ch_ubam.mix(FGBIO_COPYUMIFROMREADNAME.out.bam) } else { // Case 2: UMI_in_sequence - seq_fastq = ch_input_fastq.map { meta, r1, r2 -> tuple(meta, [r1, r2]) } + ch_fastq_seq = ch_input_fastq.map { meta, r1, r2 -> tuple(meta, [r1, r2]) } - FASTQTOBAM_SEQ(seq_fastq) + FASTQTOBAM_SEQ(ch_fastq_seq) ch_versions = ch_versions.mix(FASTQTOBAM_SEQ.out.versions) - ch_fastq_to_uBAM = ch_fastq_to_uBAM.mix(FASTQTOBAM_SEQ.out.bam) + ch_ubam = ch_ubam.mix(FASTQTOBAM_SEQ.out.bam) } // 1.2: uBAM => Mapped BAM -/* - // Seq branch => uBAM + SAMTOOLS_FASTQ(ch_ubam) + ch_versions = ch_versions.mix(SAMTOOLS_FASTQ.out.versions) - // 1.1: FASTQ => uBAM - SEQ_FQ = ch_input_fastq_branch.umi_in_seq.map { meta, r1, r2, _f -> tuple(meta, [r1, r2]) } + def ch_bwa_index = Channel.value(file(params.genomes.GRCh38.bwamem)) + def ch_fasta = Channel.value(file(params.genomes.GRCh38.fasta)) - FASTQTOBAM_SEQ(SEQ_FQ) + BWA_MEM( + SAMTOOLS_FASTQ.out.reads, + ch_bwa_index, + ch_fasta, + false + ) + ch_versions = ch_versions.mix(BWA_MEM.out.versions) - // 1.2: uBAM -> Mapped BAM -*/ + FGBIO_ZIPPERBAMS( + ch_ubam, + BWA_MEM.out.bam, + ch_fasta + ) + ch_versions = ch_versions.mix(FGBIO_ZIPPERBAMS.out.versions) emit: /* @@ -81,6 +91,6 @@ workflow CONSENSUS { duplex_metrics = ch_duplex_metrics versions = ch_versions */ - consensus_bam = Channel.empty() + consensus_bam = FGBIO_ZIPPERBAMS.out.bam versions = ch_versions } diff --git a/workflows/preprocessing.nf b/workflows/preprocessing.nf index 55a270f1..8f17430b 100644 --- a/workflows/preprocessing.nf +++ b/workflows/preprocessing.nf @@ -128,7 +128,7 @@ workflow PREPROCESSING { rg = rg + [ 'SM': samplename, 'LB': meta.library ?: "", 'PL': meta.platform ?: rg.PL, - 'ID': meta.readgroup ?: rg.ID + 'ID': (meta.readgroup ?: rg.ID ?: meta.id ?: samplename) ] def meta_with_readgroup = meta + ['single_end': single_end, 'readgroup': rg] return [meta_with_readgroup, fastq] @@ -159,8 +159,11 @@ if (params.enable_umi) { ch_versions = ch_versions.mix(CONSENSUS.out.versions) + def ch_ubam_for_umi = CONSENSUS.out.ubam + def ch_mapped_umi_bam = CONSENSUS.out.mapped_bam + // Output channels - def ch_umi_consensus_bam = CONSENSUS.out.consensus_bam + def ch_umi_consensus_bam = ch_mapped_umi_bam } /* From 2a815de74cb66c023ede4ea6a6624621a7954742 Mon Sep 17 00:00:00 2001 From: Dhamar Date: Wed, 20 Aug 2025 16:38:14 +0200 Subject: [PATCH 09/33] Change readgroup ID handling, and alignment reference and method --- subworkflows/local/consensus/main.nf | 42 +++++++++++---- workflows/preprocessing.nf | 79 ++++++++++++++++------------ 2 files changed, 78 insertions(+), 43 deletions(-) diff --git a/subworkflows/local/consensus/main.nf b/subworkflows/local/consensus/main.nf index 2a149692..7dedde3e 100644 --- a/subworkflows/local/consensus/main.nf +++ b/subworkflows/local/consensus/main.nf @@ -11,6 +11,8 @@ include { FGBIO_ZIPPERBAMS } from '../../../modules/nf-co include { SAMTOOLS_FASTQ } from '../../../modules/nf-core/samtools/fastq/main' include { SAMTOOLS_INDEX } from '../../../modules/nf-core/samtools/index/main' include { BWA_MEM } from '../../../modules/nf-core/bwa/mem/main' +include { BWAMEM2_MEM } from '../../../modules/nf-core/bwamem2/mem/main' +include { BOWTIE2_ALIGN } from '../../../modules/nf-core/bowtie2/align/main' workflow CONSENSUS { @@ -67,24 +69,44 @@ workflow CONSENSUS { SAMTOOLS_FASTQ(ch_ubam) ch_versions = ch_versions.mix(SAMTOOLS_FASTQ.out.versions) - def ch_bwa_index = Channel.value(file(params.genomes.GRCh38.bwamem)) - def ch_fasta = Channel.value(file(params.genomes.GRCh38.fasta)) + def ch_reads_aligner_index_fasta = SAMTOOLS_FASTQ.out.reads.map { meta, reads -> + def gd = (meta.genome_data instanceof Map) ? meta.genome_data : [:] + def alg = (meta.aligner ?: 'bwamem') + def fasta = file(gd.fasta, checkIfExists: true) + def index = file(gd[alg], checkIfExists: true) + tuple(meta, reads, alg, index, fasta) + } + + ch_reads_aligner_index_fasta.branch { meta, reads, alg, index, fasta -> + bwamem : alg == 'bwamem' ; return [meta, reads, index, fasta] + bwamem2 : alg == 'bwamem2' ; return [meta, reads, index, fasta] + bowtie2 : alg == 'bowtie2' ; return [meta, reads, index, fasta] + other : true + }.set { ch_to_map } + + BWA_MEM(ch_to_map.bwamem, false) + BWAMEM2_MEM(ch_to_map.bwamem2, false) + BOWTIE2_ALIGN(ch_to_map.bowtie2, false, false) - BWA_MEM( - SAMTOOLS_FASTQ.out.reads, - ch_bwa_index, - ch_fasta, - false - ) ch_versions = ch_versions.mix(BWA_MEM.out.versions) + ch_versions = ch_versions.mix(BWAMEM2_MEM.out.versions) + ch_versions = ch_versions.mix(BOWTIE2_ALIGN.out.versions) + + def ch_mapped_bam = Channel.empty() + ch_mapped_bam = ch_mapped_bam.mix(BWA_MEM.out.bam) + ch_mapped_bam = ch_mapped_bam.mix(BWAMEM2_MEM.out.bam) + ch_mapped_bam = ch_mapped_bam.mix(BOWTIE2_ALIGN.out.bam) + + def ch_fasta_by_meta = ch_reads_aligner_index_fasta.map { meta, _r, _a, _i, fasta -> tuple(meta, fasta) } FGBIO_ZIPPERBAMS( ch_ubam, - BWA_MEM.out.bam, - ch_fasta + ch_mapped_bam, + ch_fasta_by_meta ) ch_versions = ch_versions.mix(FGBIO_ZIPPERBAMS.out.versions) + emit: /* consensus_bam = ch_consensus diff --git a/workflows/preprocessing.nf b/workflows/preprocessing.nf index 8f17430b..ec0920c0 100644 --- a/workflows/preprocessing.nf +++ b/workflows/preprocessing.nf @@ -128,43 +128,13 @@ workflow PREPROCESSING { rg = rg + [ 'SM': samplename, 'LB': meta.library ?: "", 'PL': meta.platform ?: rg.PL, - 'ID': (meta.readgroup ?: rg.ID ?: meta.id ?: samplename) + 'ID': rg.ID ] def meta_with_readgroup = meta + ['single_end': single_end, 'readgroup': rg] return [meta_with_readgroup, fastq] } .set {ch_input_fastq} -/* -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -// STEP: UMI CONSENSUS (optional, before alignment) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -*/ - - -if (params.enable_umi) { - - // Convert to [meta, r1, r2] for the UMI subworkflow - def ch_umi_fastq = ch_input_fastq.map { meta, reads -> - def r1 = (reads instanceof List) ? reads[0] : reads - def r2 = (reads instanceof List && reads.size() > 1) ? reads[1] : [] - return [meta, r1, r2] - } - - // Call the UMI subworkflow “like a function” - // NOTE: your CONSENSUS expects (ch_fastq, ch_reference, ch_umi_in_readname) - // If your CONSENSUS expects a single fasta (not keyed), replace CH_UMI_FASTA by Channel.fromPath(...). - - CONSENSUS(ch_umi_fastq, genomes) - - ch_versions = ch_versions.mix(CONSENSUS.out.versions) - - def ch_ubam_for_umi = CONSENSUS.out.ubam - def ch_mapped_umi_bam = CONSENSUS.out.mapped_bam - - // Output channels - def ch_umi_consensus_bam = ch_mapped_umi_bam -} /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -225,6 +195,31 @@ if (params.enable_umi) { ch_fastq_per_sample.dump(tag:"FASTQ per sample", pretty: true) +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// STEP: UMI CONSENSUS (optional, before alignment) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +*/ + + +if (params.enable_umi) { + + // Convert to [meta, r1, r2] for the UMI subworkflow + def ch_umi_fastq = ch_input_fastq.map { meta, reads -> + def r1 = (reads instanceof List) ? reads[0] : reads + def r2 = (reads instanceof List && reads.size() > 1) ? reads[1] : [] + return [meta, r1, r2] + } + + CONSENSUS(ch_umi_fastq, genomes) + + ch_versions = ch_versions.mix(CONSENSUS.out.versions) + + def ch_ubam_for_umi = CONSENSUS.out.ubam + def ch_umi_consensus_bam = CONSENSUS.out.consensus_bam +} + + /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // FASTQ TRIMMING AND QC @@ -470,7 +465,12 @@ def readgroup_from_fastq(path) { } assert line.startsWith('@') line = line.substring(1) - def fields = line.split(':') + + def parts = line.split(' ') + def left = parts[0] + def right = parts.size() > 1 ? parts[1] : "" + + def fields = left.split(':') def rg = [:] rg.CN = "CMGG" @@ -481,7 +481,11 @@ def readgroup_from_fastq(path) { def run_nubmer = fields[1] def fcid = fields[2] def lane = fields[3] - def index = fields[-1] =~ /[GATC+-]/ ? fields[-1] : "" + def index = "" + if (right) { + def r = right.split(':') + if (r && (r[-1] ==~ /[ACGTN+\-]+/)) index = r[-1] + } rg.ID = [fcid,lane].join(".") rg.PU = [fcid, lane, index].findAll().join(".") @@ -489,6 +493,15 @@ def readgroup_from_fastq(path) { } else if (fields.size() == 5) { def fcid = fields[0] rg.ID = fcid + rg.PU = fcid + rg.PL = "ILLUMINA" + } + else { + // fallback para cabeceras no-CASAVA: usa el primer campo no vacío + def fallback = (fields && fields[0]) ? fields[0] : "unknown" + rg.ID = fallback + rg.PU = fallback + rg.PL = rg.PL ?: "ILLUMINA" } return rg } From 55b35d6e530f278bb33300f98a7686700cd2652a Mon Sep 17 00:00:00 2001 From: hwjeong Date: Wed, 20 Aug 2025 16:46:22 +0200 Subject: [PATCH 10/33] samplesheet extra field: umi_type (readname/seq/none) --- assets/fastq.yml | 16 ++++++++++++++++ assets/schema_input.json | 13 ++++++++++--- 2 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 assets/fastq.yml diff --git a/assets/fastq.yml b/assets/fastq.yml new file mode 100644 index 00000000..66a04f03 --- /dev/null +++ b/assets/fastq.yml @@ -0,0 +1,16 @@ +# fastq inputs with UMI +- id: umi_sample1 + samplename: umi_test1 + library: test + organism: Homo sapiens + tag: WES + fastq_1: https://raw.githubusercontent.com/nf-core/test-datasets/modules/data/genomics/homo_sapiens/illumina/fastq/test.umi_1.fastq.gz + fastq_2: https://raw.githubusercontent.com/nf-core/test-datasets/modules/data/genomics/homo_sapiens/illumina/fastq/test.umi_2.fastq.gz + +- id: umi_sample2 + samplename: umi_test2 + library: test + organism: Homo sapiens + tag: WES + fastq_1: https://raw.githubusercontent.com/nf-core/test-datasets/modules/data/genomics/homo_sapiens/illumina/fastq/test2.umi_1.fastq.gz + fastq_2: https://raw.githubusercontent.com/nf-core/test-datasets/modules/data/genomics/homo_sapiens/illumina/fastq/test2.umi_2.fastq.gz diff --git a/assets/schema_input.json b/assets/schema_input.json index beaa2eb6..73e9e721 100644 --- a/assets/schema_input.json +++ b/assets/schema_input.json @@ -72,6 +72,13 @@ "pattern": "^[a-zA-Z0-9_]+.bed$", "default": null }, + "umi_type": { + "meta": ["umi_type"], + "type": "string", + "description": "Distinguish UMI samples (readname, seq) from non-UMI samples", + "enum": ["readname", "seq", "none"], + "default": "none" + }, "lane": { "type": "integer", "meta": ["lane"], @@ -110,13 +117,13 @@ }, "anyOf": [ { - "required": ["id", "samplename", "organism", "tag", "fastq_1", "fastq_2"] + "required": ["id", "samplename", "organism", "tag", "fastq_1", "fastq_2", "umi_type"] }, { - "required": ["id", "samplename", "genome", "tag", "fastq_1", "fastq_2"] + "required": ["id", "samplename", "genome", "tag", "fastq_1", "fastq_2", "umi_type"] }, { - "required": ["id", "samplesheet", "sample_info", "flowcell"] + "required": ["id", "samplesheet", "sample_info", "flowcell", "umi_type"] } ] }, From 2b4c13f83e9731e6f3ab50897092fe82c6667e1e Mon Sep 17 00:00:00 2001 From: Dhamar Date: Thu, 21 Aug 2025 09:29:39 +0200 Subject: [PATCH 11/33] Correction: alignment with subworkflow --- subworkflows/local/consensus/main.nf | 26 ++++---------------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/subworkflows/local/consensus/main.nf b/subworkflows/local/consensus/main.nf index 7dedde3e..71285b64 100644 --- a/subworkflows/local/consensus/main.nf +++ b/subworkflows/local/consensus/main.nf @@ -10,9 +10,7 @@ include { FGBIO_SORTBAM } from '../../../modules/nf-co include { FGBIO_ZIPPERBAMS } from '../../../modules/nf-core/fgbio/zipperbams/main' include { SAMTOOLS_FASTQ } from '../../../modules/nf-core/samtools/fastq/main' include { SAMTOOLS_INDEX } from '../../../modules/nf-core/samtools/index/main' -include { BWA_MEM } from '../../../modules/nf-core/bwa/mem/main' -include { BWAMEM2_MEM } from '../../../modules/nf-core/bwamem2/mem/main' -include { BOWTIE2_ALIGN } from '../../../modules/nf-core/bowtie2/align/main' +include { FASTQ_ALIGN_DNA } from '../../../subworkflows/nf-core/fastq_align_dna/main' workflow CONSENSUS { @@ -77,26 +75,10 @@ workflow CONSENSUS { tuple(meta, reads, alg, index, fasta) } - ch_reads_aligner_index_fasta.branch { meta, reads, alg, index, fasta -> - bwamem : alg == 'bwamem' ; return [meta, reads, index, fasta] - bwamem2 : alg == 'bwamem2' ; return [meta, reads, index, fasta] - bowtie2 : alg == 'bowtie2' ; return [meta, reads, index, fasta] - other : true - }.set { ch_to_map } - - BWA_MEM(ch_to_map.bwamem, false) - BWAMEM2_MEM(ch_to_map.bwamem2, false) - BOWTIE2_ALIGN(ch_to_map.bowtie2, false, false) - - ch_versions = ch_versions.mix(BWA_MEM.out.versions) - ch_versions = ch_versions.mix(BWAMEM2_MEM.out.versions) - ch_versions = ch_versions.mix(BOWTIE2_ALIGN.out.versions) - - def ch_mapped_bam = Channel.empty() - ch_mapped_bam = ch_mapped_bam.mix(BWA_MEM.out.bam) - ch_mapped_bam = ch_mapped_bam.mix(BWAMEM2_MEM.out.bam) - ch_mapped_bam = ch_mapped_bam.mix(BOWTIE2_ALIGN.out.bam) + FASTQ_ALIGN_DNA(ch_reads_aligner_index_fasta, false) + ch_versions = ch_versions.mix(FASTQ_ALIGN_DNA.out.versions) + def ch_mapped_bam = FASTQ_ALIGN_DNA.out.bam def ch_fasta_by_meta = ch_reads_aligner_index_fasta.map { meta, _r, _a, _i, fasta -> tuple(meta, fasta) } FGBIO_ZIPPERBAMS( From a3b44756a10dd6cca6aef7f91ad26e5b9a048c49 Mon Sep 17 00:00:00 2001 From: Hwanseok Jeong Date: Thu, 21 Aug 2025 09:34:53 +0200 Subject: [PATCH 12/33] Delete assets/fastq.yml Mistakenly committed file --- assets/fastq.yml | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 assets/fastq.yml diff --git a/assets/fastq.yml b/assets/fastq.yml deleted file mode 100644 index 66a04f03..00000000 --- a/assets/fastq.yml +++ /dev/null @@ -1,16 +0,0 @@ -# fastq inputs with UMI -- id: umi_sample1 - samplename: umi_test1 - library: test - organism: Homo sapiens - tag: WES - fastq_1: https://raw.githubusercontent.com/nf-core/test-datasets/modules/data/genomics/homo_sapiens/illumina/fastq/test.umi_1.fastq.gz - fastq_2: https://raw.githubusercontent.com/nf-core/test-datasets/modules/data/genomics/homo_sapiens/illumina/fastq/test.umi_2.fastq.gz - -- id: umi_sample2 - samplename: umi_test2 - library: test - organism: Homo sapiens - tag: WES - fastq_1: https://raw.githubusercontent.com/nf-core/test-datasets/modules/data/genomics/homo_sapiens/illumina/fastq/test2.umi_1.fastq.gz - fastq_2: https://raw.githubusercontent.com/nf-core/test-datasets/modules/data/genomics/homo_sapiens/illumina/fastq/test2.umi_2.fastq.gz From 8815f0f675da54e4c6656828ee7e030d28c61207 Mon Sep 17 00:00:00 2001 From: hwjeong Date: Thu, 21 Aug 2025 12:08:27 +0200 Subject: [PATCH 13/33] umi_type into meta --- nextflow_schema.json | 47 +++++++++++++++++----------- subworkflows/local/consensus/main.nf | 35 +++++++++------------ workflows/preprocessing.nf | 10 ++---- 3 files changed, 46 insertions(+), 46 deletions(-) diff --git a/nextflow_schema.json b/nextflow_schema.json index 051a17a3..53af1851 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -10,7 +10,10 @@ "type": "object", "fa_icon": "fas fa-terminal", "description": "Define where the pipeline should find input data and save output data.", - "required": ["input", "outdir"], + "required": [ + "input", + "outdir" + ], "properties": { "input": { "type": "string", @@ -47,29 +50,28 @@ "description": "", "default": "", "properties": { - "enable_umi": { - "type": "boolean", - "description": "Enable UMI subworkflow", - "hidden": true, - "default": true - }, - "umi_in_readname": { - "type": "boolean", - "description": "UMI present in ireadname", - "hidden": true, - "default": true - }, "aligner": { "type": "string", "default": "bowtie2", "description": "Which aligner to use", - "enum": ["bowtie2", "bwamem", "bwamem2", "dragmap", "snap", "star"] + "enum": [ + "bowtie2", + "bwamem", + "bwamem2", + "dragmap", + "snap", + "star" + ] }, "markdup": { "type": "string", "default": "bamsormadup", "description": "Which alignment postprocessor to use", - "enum": ["bamsormadup", "samtools", "false"] + "enum": [ + "bamsormadup", + "samtools", + "false" + ] }, "umi_aware": { "type": "boolean", @@ -117,7 +119,9 @@ "description": "Directory containing gene list bed files for granular coverage analysis" } }, - "required": ["aligner"] + "required": [ + "aligner" + ] }, "institutional_config_options": { "title": "Institutional config options", @@ -192,7 +196,14 @@ "description": "Method used to save pipeline results to output directory.", "help_text": "The Nextflow `publishDir` option specifies which intermediate files should be saved to the output directory. This option tells the pipeline what method should be used to move these files. See [Nextflow docs](https://www.nextflow.io/docs/latest/process.html#publishdir) for details.", "fa_icon": "fas fa-copy", - "enum": ["symlink", "rellink", "link", "copy", "copyNoFollow", "move"], + "enum": [ + "symlink", + "rellink", + "link", + "copy", + "copyNoFollow", + "move" + ], "hidden": true }, "email_on_fail": { @@ -272,4 +283,4 @@ "$ref": "#/$defs/generic_options" } ] -} +} \ No newline at end of file diff --git a/subworkflows/local/consensus/main.nf b/subworkflows/local/consensus/main.nf index 71285b64..df41796d 100644 --- a/subworkflows/local/consensus/main.nf +++ b/subworkflows/local/consensus/main.nf @@ -15,26 +15,26 @@ include { FASTQ_ALIGN_DNA } from '../../../subworkflows/ workflow CONSENSUS { take: - ch_input_fastq // channel: [meta_with_readgroup, fastq] for SE/PE/duplex samples - ch_genomes // map: reference genome files + ch_umi_fastq // channel: [meta_with_readgroup, fastq] for SE/PE/duplex samples main: def ch_versions = Channel.empty() - def ch_fastq_readname = Channel.empty() - def ch_fastq_seq = Channel.empty() def ch_ubam = Channel.empty() - def ch_ubam_with_bai = Channel.empty() - // 1.1: FASTQ => uBAM - - if (params.umi_in_readname) { - // Case 1: UMI_in_readname + // 1.1: FASTQ => uBAM - ch_fastq_readname = ch_input_fastq.map { meta, r1, r2 -> tuple(meta, [r1, r2]) } + def ch_fastq = ch_umi_fastq + .map { meta, r1, r2 -> tuple(meta, [r1, r2]) } + .branch { meta, _fastq -> + readname: meta['umi_type'] == 'readname' + seq: meta['umi_type'] == 'seq' + } - FASTQTOBAM_READNAME(ch_fastq_readname) + // Case 1: UMI_in_readname + if (ch_fastq.readname) { + FASTQTOBAM_READNAME(ch_fastq.readname) ch_versions = ch_versions.mix(FASTQTOBAM_READNAME.out.versions) SAMTOOLS_INDEX(FASTQTOBAM_READNAME.out.bam) @@ -49,14 +49,12 @@ workflow CONSENSUS { ch_versions = ch_versions.mix(FGBIO_COPYUMIFROMREADNAME.out.versions) ch_ubam = ch_ubam.mix(FGBIO_COPYUMIFROMREADNAME.out.bam) - - } else { + } // Case 2: UMI_in_sequence - ch_fastq_seq = ch_input_fastq.map { meta, r1, r2 -> tuple(meta, [r1, r2]) } - - FASTQTOBAM_SEQ(ch_fastq_seq) + if (ch_fastq.seq) { + FASTQTOBAM_SEQ(ch_fastq.seq) ch_versions = ch_versions.mix(FASTQTOBAM_SEQ.out.versions) ch_ubam = ch_ubam.mix(FASTQTOBAM_SEQ.out.bam) @@ -90,11 +88,6 @@ workflow CONSENSUS { emit: - /* - consensus_bam = ch_consensus - duplex_metrics = ch_duplex_metrics - versions = ch_versions - */ consensus_bam = FGBIO_ZIPPERBAMS.out.bam versions = ch_versions } diff --git a/workflows/preprocessing.nf b/workflows/preprocessing.nf index ec0920c0..a903fd88 100644 --- a/workflows/preprocessing.nf +++ b/workflows/preprocessing.nf @@ -201,23 +201,19 @@ workflow PREPROCESSING { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ - -if (params.enable_umi) { - // Convert to [meta, r1, r2] for the UMI subworkflow - def ch_umi_fastq = ch_input_fastq.map { meta, reads -> + def ch_umi_fastq = ch_fastq_per_sample.map { meta, reads -> def r1 = (reads instanceof List) ? reads[0] : reads def r2 = (reads instanceof List && reads.size() > 1) ? reads[1] : [] return [meta, r1, r2] } + .filter {meta, _r1, _r2 -> meta.umi_type != "none"} - CONSENSUS(ch_umi_fastq, genomes) - + CONSENSUS(ch_umi_fastq) ch_versions = ch_versions.mix(CONSENSUS.out.versions) def ch_ubam_for_umi = CONSENSUS.out.ubam def ch_umi_consensus_bam = CONSENSUS.out.consensus_bam -} /* From 405653deac0ee240dbe7d6144ea78c9c35216ebe Mon Sep 17 00:00:00 2001 From: hwjeong Date: Thu, 21 Aug 2025 13:18:41 +0200 Subject: [PATCH 14/33] deleted umi params --- nextflow.config | 5 ----- 1 file changed, 5 deletions(-) diff --git a/nextflow.config b/nextflow.config index 885e5f5a..cd44c534 100644 --- a/nextflow.config +++ b/nextflow.config @@ -26,11 +26,6 @@ params { roi = null genelists = null - // --- UMI options --- - enable_umi = false // turn UMI subworkflow on/off - umi_in_readname = true // true: UMI in read name; false: in sequence (future) - - // References genomes = [:] igenomes_base = '/references' From b150d3187a3437069aa090f2b7d930e54ba38029 Mon Sep 17 00:00:00 2001 From: hwjeong Date: Thu, 21 Aug 2025 15:14:36 +0200 Subject: [PATCH 15/33] successful run until step 1.2 --- subworkflows/local/consensus/main.nf | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/subworkflows/local/consensus/main.nf b/subworkflows/local/consensus/main.nf index df41796d..d6e262a1 100644 --- a/subworkflows/local/consensus/main.nf +++ b/subworkflows/local/consensus/main.nf @@ -12,7 +12,6 @@ include { SAMTOOLS_FASTQ } from '../../../modules/nf-co include { SAMTOOLS_INDEX } from '../../../modules/nf-core/samtools/index/main' include { FASTQ_ALIGN_DNA } from '../../../subworkflows/nf-core/fastq_align_dna/main' - workflow CONSENSUS { take: ch_umi_fastq // channel: [meta_with_readgroup, fastq] for SE/PE/duplex samples @@ -62,10 +61,11 @@ workflow CONSENSUS { // 1.2: uBAM => Mapped BAM - SAMTOOLS_FASTQ(ch_ubam) + SAMTOOLS_FASTQ(ch_ubam, true) + ch_versions = ch_versions.mix(SAMTOOLS_FASTQ.out.versions) - def ch_reads_aligner_index_fasta = SAMTOOLS_FASTQ.out.reads.map { meta, reads -> + def ch_reads_aligner_index_fasta = SAMTOOLS_FASTQ.out.interleaved.map { meta, reads -> def gd = (meta.genome_data instanceof Map) ? meta.genome_data : [:] def alg = (meta.aligner ?: 'bwamem') def fasta = file(gd.fasta, checkIfExists: true) @@ -76,18 +76,34 @@ workflow CONSENSUS { FASTQ_ALIGN_DNA(ch_reads_aligner_index_fasta, false) ch_versions = ch_versions.mix(FASTQ_ALIGN_DNA.out.versions) - def ch_mapped_bam = FASTQ_ALIGN_DNA.out.bam + def ch_mapped_bam = FASTQ_ALIGN_DNA.out.bam.map { meta, bam -> + def sample = meta.samplename ?: UUID.randomUUID().toString() + def new_bam = file("${sample}.mapped.bam") + bam.copyTo(new_bam) + tuple(meta, new_bam) + } + def ch_fasta_by_meta = ch_reads_aligner_index_fasta.map { meta, _r, _a, _i, fasta -> tuple(meta, fasta) } + def ch_dict_by_meta = ch_reads_aligner_index_fasta.map { meta, _r, _a, _i, _fasta -> + def dict = file(meta.genome_data.dict, checkIfExists: true) + tuple(meta, dict) + } + FGBIO_ZIPPERBAMS( ch_ubam, ch_mapped_bam, - ch_fasta_by_meta + ch_fasta_by_meta, + ch_dict_by_meta ) + ch_versions = ch_versions.mix(FGBIO_ZIPPERBAMS.out.versions) + // 1.3: Mapped BAM -> Grouped BAM + emit: + ubam = ch_ubam consensus_bam = FGBIO_ZIPPERBAMS.out.bam versions = ch_versions } From 4f2c54babfd57b3646435d67017aff7a21f02d45 Mon Sep 17 00:00:00 2001 From: hwjeong Date: Thu, 21 Aug 2025 16:39:33 +0200 Subject: [PATCH 16/33] zipperbam input fixed --- .../fgbio/zipperbams/fgbio-zipperbams.diff | 40 +++++++++++++++++++ modules/nf-core/fgbio/zipperbams/main.nf | 8 +--- subworkflows/local/consensus/main.nf | 16 ++++---- 3 files changed, 51 insertions(+), 13 deletions(-) create mode 100644 modules/nf-core/fgbio/zipperbams/fgbio-zipperbams.diff diff --git a/modules/nf-core/fgbio/zipperbams/fgbio-zipperbams.diff b/modules/nf-core/fgbio/zipperbams/fgbio-zipperbams.diff new file mode 100644 index 00000000..48614230 --- /dev/null +++ b/modules/nf-core/fgbio/zipperbams/fgbio-zipperbams.diff @@ -0,0 +1,40 @@ +Changes in component 'nf-core/fgbio/zipperbams' +'modules/nf-core/fgbio/zipperbams/environment.yml' is unchanged +Changes in 'fgbio/zipperbams/main.nf': +--- modules/nf-core/fgbio/zipperbams/main.nf ++++ modules/nf-core/fgbio/zipperbams/main.nf +@@ -8,10 +8,8 @@ + 'community.wave.seqera.io/library/fgbio:2.5.21--368dab1b4f308243' }" + + input: +- tuple val(meta), path(unmapped_bam) +- tuple val(meta2), path(mapped_bam) +- tuple val(meta3), path(fasta) +- tuple val(meta4), path(dict) ++ ++ tuple val(meta), path(unmapped_bam), path(mapped_bam), path(fasta), path(dict) + + output: + tuple val(meta), path("${prefix}.bam"), emit: bam +@@ -22,7 +20,6 @@ + + script: + def args = task.ext.args ?: '' +- def args2 = task.ext.args2 ?: '' + def compression = task.ext.compression ?: '0' + prefix = task.ext.prefix ?: "${meta.id}_zipped" + def mem_gb = 8 +@@ -50,7 +47,6 @@ + ${args} \\ + --output ${prefix}.bam + +- + cat <<-END_VERSIONS > versions.yml + "${task.process}": + fgbio: \$( echo \$(fgbio --version 2>&1 | tr -d '[:cntrl:]' ) | sed -e 's/^.*Version: //;s/\\[.*\$//') + +'modules/nf-core/fgbio/zipperbams/meta.yml' is unchanged +'modules/nf-core/fgbio/zipperbams/tests/main.nf.test' is unchanged +'modules/nf-core/fgbio/zipperbams/tests/main.nf.test.snap' is unchanged +'modules/nf-core/fgbio/zipperbams/tests/nextflow.config' is unchanged +************************************************************ diff --git a/modules/nf-core/fgbio/zipperbams/main.nf b/modules/nf-core/fgbio/zipperbams/main.nf index bf523804..cb723439 100644 --- a/modules/nf-core/fgbio/zipperbams/main.nf +++ b/modules/nf-core/fgbio/zipperbams/main.nf @@ -8,10 +8,8 @@ process FGBIO_ZIPPERBAMS { 'community.wave.seqera.io/library/fgbio:2.5.21--368dab1b4f308243' }" input: - tuple val(meta), path(unmapped_bam) - tuple val(meta2), path(mapped_bam) - tuple val(meta3), path(fasta) - tuple val(meta4), path(dict) + + tuple val(meta), path(unmapped_bam), path(mapped_bam), path(fasta), path(dict) output: tuple val(meta), path("${prefix}.bam"), emit: bam @@ -22,7 +20,6 @@ process FGBIO_ZIPPERBAMS { script: def args = task.ext.args ?: '' - def args2 = task.ext.args2 ?: '' def compression = task.ext.compression ?: '0' prefix = task.ext.prefix ?: "${meta.id}_zipped" def mem_gb = 8 @@ -50,7 +47,6 @@ process FGBIO_ZIPPERBAMS { ${args} \\ --output ${prefix}.bam - cat <<-END_VERSIONS > versions.yml "${task.process}": fgbio: \$( echo \$(fgbio --version 2>&1 | tr -d '[:cntrl:]' ) | sed -e 's/^.*Version: //;s/\\[.*\$//') diff --git a/subworkflows/local/consensus/main.nf b/subworkflows/local/consensus/main.nf index d6e262a1..94358f99 100644 --- a/subworkflows/local/consensus/main.nf +++ b/subworkflows/local/consensus/main.nf @@ -20,7 +20,6 @@ workflow CONSENSUS { def ch_versions = Channel.empty() def ch_ubam = Channel.empty() - // 1.1: FASTQ => uBAM @@ -90,12 +89,15 @@ workflow CONSENSUS { tuple(meta, dict) } - FGBIO_ZIPPERBAMS( - ch_ubam, - ch_mapped_bam, - ch_fasta_by_meta, - ch_dict_by_meta - ) + + ch_ubam + .join(ch_mapped_bam, by:0) + .join(ch_fasta_by_meta, by:0) + .join(ch_dict_by_meta, by:0) + .map { meta, ubam, mapped_bam, fasta, dict -> tuple(meta, ubam, mapped_bam, fasta, dict) } + .set { ch_zipperbam } + + FGBIO_ZIPPERBAMS(ch_zipperbam) ch_versions = ch_versions.mix(FGBIO_ZIPPERBAMS.out.versions) From 1c8ff25306e2e2b407a7378ec0af196ca00d73c2 Mon Sep 17 00:00:00 2001 From: hwjeong Date: Thu, 21 Aug 2025 17:25:01 +0200 Subject: [PATCH 17/33] changing ubam output file name for successful run of zipperbam --- .../fgbio-copyumifromreadname.diff | 22 ++++++++++++++++ .../nf-core/fgbio/copyumifromreadname/main.nf | 4 +-- .../fgbio/fastqtobam/fgbio-fastqtobam.diff | 25 +++++++++++++++++++ modules/nf-core/fgbio/fastqtobam/main.nf | 4 +-- subworkflows/local/consensus/main.nf | 9 +------ 5 files changed, 52 insertions(+), 12 deletions(-) create mode 100644 modules/nf-core/fgbio/copyumifromreadname/fgbio-copyumifromreadname.diff create mode 100644 modules/nf-core/fgbio/fastqtobam/fgbio-fastqtobam.diff diff --git a/modules/nf-core/fgbio/copyumifromreadname/fgbio-copyumifromreadname.diff b/modules/nf-core/fgbio/copyumifromreadname/fgbio-copyumifromreadname.diff new file mode 100644 index 00000000..7a934f9f --- /dev/null +++ b/modules/nf-core/fgbio/copyumifromreadname/fgbio-copyumifromreadname.diff @@ -0,0 +1,22 @@ +Changes in component 'nf-core/fgbio/copyumifromreadname' +'modules/nf-core/fgbio/copyumifromreadname/environment.yml' is unchanged +Changes in 'fgbio/copyumifromreadname/main.nf': +--- modules/nf-core/fgbio/copyumifromreadname/main.nf ++++ modules/nf-core/fgbio/copyumifromreadname/main.nf +@@ -11,8 +11,8 @@ + tuple val(meta), path(bam), path(bai) + + output: +- tuple val(meta), path("*.bam"), emit: bam +- tuple val(meta), path("*.bai"), emit: bai ++ tuple val(meta), path("${meta.samplename}_ubam.bam"), emit: bam ++ tuple val(meta), path("${meta.samplename}_ubai.bai"), emit: bai + path "versions.yml" , emit: versions + + when: + +'modules/nf-core/fgbio/copyumifromreadname/meta.yml' is unchanged +'modules/nf-core/fgbio/copyumifromreadname/tests/main.nf.test' is unchanged +'modules/nf-core/fgbio/copyumifromreadname/tests/main.nf.test.snap' is unchanged +'modules/nf-core/fgbio/copyumifromreadname/tests/nextflow.config' is unchanged +************************************************************ diff --git a/modules/nf-core/fgbio/copyumifromreadname/main.nf b/modules/nf-core/fgbio/copyumifromreadname/main.nf index b15c970a..62f04904 100644 --- a/modules/nf-core/fgbio/copyumifromreadname/main.nf +++ b/modules/nf-core/fgbio/copyumifromreadname/main.nf @@ -11,8 +11,8 @@ process FGBIO_COPYUMIFROMREADNAME { tuple val(meta), path(bam), path(bai) output: - tuple val(meta), path("*.bam"), emit: bam - tuple val(meta), path("*.bai"), emit: bai + tuple val(meta), path("${meta.samplename}_ubam.bam"), emit: bam + tuple val(meta), path("${meta.samplename}_ubai.bai"), emit: bai path "versions.yml" , emit: versions when: diff --git a/modules/nf-core/fgbio/fastqtobam/fgbio-fastqtobam.diff b/modules/nf-core/fgbio/fastqtobam/fgbio-fastqtobam.diff new file mode 100644 index 00000000..bac2fe9a --- /dev/null +++ b/modules/nf-core/fgbio/fastqtobam/fgbio-fastqtobam.diff @@ -0,0 +1,25 @@ +Changes in component 'nf-core/fgbio/fastqtobam' +'modules/nf-core/fgbio/fastqtobam/environment.yml' is unchanged +Changes in 'fgbio/fastqtobam/main.nf': +--- modules/nf-core/fgbio/fastqtobam/main.nf ++++ modules/nf-core/fgbio/fastqtobam/main.nf +@@ -11,8 +11,8 @@ + tuple val(meta), path(reads) + + output: +- tuple val(meta), path("*.bam") , emit: bam , optional: true +- tuple val(meta), path("*.cram"), emit: cram, optional: true ++ tuple val(meta), path("${meta.samplename}_ubam.bam") , emit: bam , optional: true ++ tuple val(meta), path("${meta.samplename}_ucram.cram"), emit: cram, optional: true + path "versions.yml" , emit: versions + + when: + +'modules/nf-core/fgbio/fastqtobam/meta.yml' is unchanged +'modules/nf-core/fgbio/fastqtobam/tests/bam.config' is unchanged +'modules/nf-core/fgbio/fastqtobam/tests/cram.config' is unchanged +'modules/nf-core/fgbio/fastqtobam/tests/custom_sample.config' is unchanged +'modules/nf-core/fgbio/fastqtobam/tests/main.nf.test' is unchanged +'modules/nf-core/fgbio/fastqtobam/tests/main.nf.test.snap' is unchanged +'modules/nf-core/fgbio/fastqtobam/tests/umi.config' is unchanged +************************************************************ diff --git a/modules/nf-core/fgbio/fastqtobam/main.nf b/modules/nf-core/fgbio/fastqtobam/main.nf index 6ee64bb3..ff8dba76 100644 --- a/modules/nf-core/fgbio/fastqtobam/main.nf +++ b/modules/nf-core/fgbio/fastqtobam/main.nf @@ -11,8 +11,8 @@ process FGBIO_FASTQTOBAM { tuple val(meta), path(reads) output: - tuple val(meta), path("*.bam") , emit: bam , optional: true - tuple val(meta), path("*.cram"), emit: cram, optional: true + tuple val(meta), path("${meta.samplename}_ubam.bam") , emit: bam , optional: true + tuple val(meta), path("${meta.samplename}_ucram.cram"), emit: cram, optional: true path "versions.yml" , emit: versions when: diff --git a/subworkflows/local/consensus/main.nf b/subworkflows/local/consensus/main.nf index 94358f99..c5acc58f 100644 --- a/subworkflows/local/consensus/main.nf +++ b/subworkflows/local/consensus/main.nf @@ -75,12 +75,7 @@ workflow CONSENSUS { FASTQ_ALIGN_DNA(ch_reads_aligner_index_fasta, false) ch_versions = ch_versions.mix(FASTQ_ALIGN_DNA.out.versions) - def ch_mapped_bam = FASTQ_ALIGN_DNA.out.bam.map { meta, bam -> - def sample = meta.samplename ?: UUID.randomUUID().toString() - def new_bam = file("${sample}.mapped.bam") - bam.copyTo(new_bam) - tuple(meta, new_bam) - } + def ch_mapped_bam = FASTQ_ALIGN_DNA.out.bam def ch_fasta_by_meta = ch_reads_aligner_index_fasta.map { meta, _r, _a, _i, fasta -> tuple(meta, fasta) } @@ -88,8 +83,6 @@ workflow CONSENSUS { def dict = file(meta.genome_data.dict, checkIfExists: true) tuple(meta, dict) } - - ch_ubam .join(ch_mapped_bam, by:0) .join(ch_fasta_by_meta, by:0) From ea903eba6c579d1ef7a3960604a52a60b026ca93 Mon Sep 17 00:00:00 2001 From: Dhamar Date: Fri, 22 Aug 2025 09:26:08 +0200 Subject: [PATCH 18/33] Step 1.3 Mapped BAM => Grouped BAM --- subworkflows/local/consensus/main.nf | 17 ++++++++++++++--- workflows/preprocessing.nf | 4 +++- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/subworkflows/local/consensus/main.nf b/subworkflows/local/consensus/main.nf index c5acc58f..18223b4c 100644 --- a/subworkflows/local/consensus/main.nf +++ b/subworkflows/local/consensus/main.nf @@ -15,14 +15,13 @@ include { FASTQ_ALIGN_DNA } from '../../../subworkflows/ workflow CONSENSUS { take: ch_umi_fastq // channel: [meta_with_readgroup, fastq] for SE/PE/duplex samples - + main: def ch_versions = Channel.empty() def ch_ubam = Channel.empty() // 1.1: FASTQ => uBAM - def ch_fastq = ch_umi_fastq .map { meta, r1, r2 -> tuple(meta, [r1, r2]) } .branch { meta, _fastq -> @@ -93,12 +92,24 @@ workflow CONSENSUS { FGBIO_ZIPPERBAMS(ch_zipperbam) ch_versions = ch_versions.mix(FGBIO_ZIPPERBAMS.out.versions) + + + // 1.3: Mapped BAM => Grouped BAM + + // 1.3: Mapped BAM => Grouped BAM + def ch_strategy = Channel.value( (params.umi_group_strategy ?: 'Adjacency') as String ) - // 1.3: Mapped BAM -> Grouped BAM + FGBIO_GROUPREADSBYUMI( + FGBIO_ZIPPERBAMS.out.bam, + ch_strategy + ) + ch_versions = ch_versions.mix(FGBIO_GROUPREADSBYUMI.out.versions) + def ch_grouped_bam = FGBIO_GROUPREADSBYUMI.out.bam emit: ubam = ch_ubam consensus_bam = FGBIO_ZIPPERBAMS.out.bam + grouped_bam = ch_grouped_bam versions = ch_versions } diff --git a/workflows/preprocessing.nf b/workflows/preprocessing.nf index a903fd88..ec95ab63 100644 --- a/workflows/preprocessing.nf +++ b/workflows/preprocessing.nf @@ -197,7 +197,7 @@ workflow PREPROCESSING { /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -// STEP: UMI CONSENSUS (optional, before alignment) +// STEP: UMI CONSENSUS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ @@ -214,6 +214,8 @@ workflow PREPROCESSING { def ch_ubam_for_umi = CONSENSUS.out.ubam def ch_umi_consensus_bam = CONSENSUS.out.consensus_bam + def ch_umi_grouped_bam = CONSENSUS.out.grouped_bam + /* From 7b79dc68aa19ca89429eab3761233b88af81e9a7 Mon Sep 17 00:00:00 2001 From: Hwanseok Jeong Date: Fri, 22 Aug 2025 10:06:20 +0200 Subject: [PATCH 19/33] modules.json update (manual) --- modules.json | 170 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 129 insertions(+), 41 deletions(-) diff --git a/modules.json b/modules.json index 9c8edb11..fd3bc419 100644 --- a/modules.json +++ b/modules.json @@ -8,199 +8,277 @@ "bcl2fastq": { "branch": "master", "git_sha": "05954dab2ff481bcb999f24455da29a5828af08d", - "installed_by": ["bcl_demultiplex"] + "installed_by": [ + "bcl_demultiplex" + ] }, "bclconvert": { "branch": "master", "git_sha": "27cceb2eb8aa959d4a8819caab886386a59a3789", - "installed_by": ["bcl_demultiplex", "modules"] + "installed_by": [ + "bcl_demultiplex", + "modules" + ] }, "biobambam/bamsormadup": { "branch": "master", "git_sha": "666652151335353eef2fcd58880bcef5bc2928e1", - "installed_by": ["modules"], + "installed_by": [ + "modules" + ], "patch": "modules/nf-core/biobambam/bamsormadup/biobambam-bamsormadup.diff" }, "bowtie2/align": { "branch": "master", "git_sha": "8864afe586537bf562eac7b83349c26207f3cb4d", - "installed_by": ["fastq_align_dna", "modules"], + "installed_by": [ + "fastq_align_dna", + "modules" + ], "patch": "modules/nf-core/bowtie2/align/bowtie2-align.diff" }, "bwa/mem": { "branch": "master", "git_sha": "a29f18660f5e3748d44d6f716241e70c942c065d", - "installed_by": ["fastq_align_dna"], + "installed_by": [ + "fastq_align_dna" + ], "patch": "modules/nf-core/bwa/mem/bwa-mem.diff" }, "bwamem2/mem": { "branch": "master", "git_sha": "a29f18660f5e3748d44d6f716241e70c942c065d", - "installed_by": ["fastq_align_dna"], + "installed_by": [ + "fastq_align_dna" + ], "patch": "modules/nf-core/bwamem2/mem/bwamem2-mem.diff" }, "dragmap/align": { "branch": "master", "git_sha": "8864afe586537bf562eac7b83349c26207f3cb4d", - "installed_by": ["fastq_align_dna"], + "installed_by": [ + "fastq_align_dna" + ], "patch": "modules/nf-core/dragmap/align/dragmap-align.diff" }, "fastp": { "branch": "master", "git_sha": "666652151335353eef2fcd58880bcef5bc2928e1", - "installed_by": ["modules"], + "installed_by": [ + "modules" + ], "patch": "modules/nf-core/fastp/fastp.diff" }, "fgbio/callduplexconsensusreads": { "branch": "master", "git_sha": "41dfa3f7c0ffabb96a6a813fe321c6d1cc5b6e46", - "installed_by": ["modules"] + "installed_by": [ + "modules" + ] }, "fgbio/callmolecularconsensusreads": { "branch": "master", "git_sha": "41dfa3f7c0ffabb96a6a813fe321c6d1cc5b6e46", - "installed_by": ["modules"] + "installed_by": [ + "modules" + ] }, "fgbio/collectduplexseqmetrics": { "branch": "master", "git_sha": "41dfa3f7c0ffabb96a6a813fe321c6d1cc5b6e46", - "installed_by": ["modules"] + "installed_by": [ + "modules" + ] }, "fgbio/copyumifromreadname": { "branch": "master", "git_sha": "47dbfc0fbcd8e4e3b73d843f4659069441ca8692", - "installed_by": ["modules"] + "installed_by": [ + "modules" + ], + "patch": "modules/nf-core/fgbio/copyumifromreadname/fgbio-copyumifromreadname.diff" }, "fgbio/fastqtobam": { "branch": "master", "git_sha": "41dfa3f7c0ffabb96a6a813fe321c6d1cc5b6e46", - "installed_by": ["modules"] + "installed_by": [ + "modules" + ], + "patch": "modules/nf-core/fgbio/fastqtobam/fgbio-fastqtobam.diff" }, "fgbio/filterconsensusreads": { "branch": "master", "git_sha": "41dfa3f7c0ffabb96a6a813fe321c6d1cc5b6e46", - "installed_by": ["modules"] + "installed_by": [ + "modules" + ] }, "fgbio/groupreadsbyumi": { "branch": "master", "git_sha": "41dfa3f7c0ffabb96a6a813fe321c6d1cc5b6e46", - "installed_by": ["modules"] + "installed_by": [ + "modules" + ] }, "fgbio/sortbam": { "branch": "master", "git_sha": "41dfa3f7c0ffabb96a6a813fe321c6d1cc5b6e46", - "installed_by": ["modules"] + "installed_by": [ + "modules" + ] }, "fgbio/zipperbams": { "branch": "master", "git_sha": "41dfa3f7c0ffabb96a6a813fe321c6d1cc5b6e46", - "installed_by": ["modules"] + "installed_by": [ + "modules" + ], + "patch": "modules/nf-core/fgbio/zipperbams/fgbio-zipperbams.diff" }, "md5sum": { "branch": "master", "git_sha": "666652151335353eef2fcd58880bcef5bc2928e1", - "installed_by": ["modules"] + "installed_by": [ + "modules" + ] }, "mosdepth": { "branch": "master", "git_sha": "666652151335353eef2fcd58880bcef5bc2928e1", - "installed_by": ["modules"], + "installed_by": [ + "modules" + ], "patch": "modules/nf-core/mosdepth/mosdepth.diff" }, "multiqc": { "branch": "master", "git_sha": "471cf3ca1617271b9b6fea09ea2ebdee78b874de", - "installed_by": ["modules"] + "installed_by": [ + "modules" + ] }, "picard/collecthsmetrics": { "branch": "master", "git_sha": "49f4e50534fe4b64101e62ea41d5dc43b1324358", - "installed_by": ["modules"], + "installed_by": [ + "modules" + ], "patch": "modules/nf-core/picard/collecthsmetrics/picard-collecthsmetrics.diff" }, "picard/collectmultiplemetrics": { "branch": "master", "git_sha": "49f4e50534fe4b64101e62ea41d5dc43b1324358", - "installed_by": ["modules"], + "installed_by": [ + "modules" + ], "patch": "modules/nf-core/picard/collectmultiplemetrics/picard-collectmultiplemetrics.diff" }, "picard/collectwgsmetrics": { "branch": "master", "git_sha": "49f4e50534fe4b64101e62ea41d5dc43b1324358", - "installed_by": ["modules"], + "installed_by": [ + "modules" + ], "patch": "modules/nf-core/picard/collectwgsmetrics/picard-collectwgsmetrics.diff" }, "samtools/cat": { "branch": "master", "git_sha": "b13f07be4c508d6ff6312d354d09f2493243e208", - "installed_by": ["modules"], + "installed_by": [ + "modules" + ], "patch": "modules/nf-core/samtools/cat/samtools-cat.diff" }, "samtools/convert": { "branch": "master", "git_sha": "b13f07be4c508d6ff6312d354d09f2493243e208", - "installed_by": ["modules"], + "installed_by": [ + "modules" + ], "patch": "modules/nf-core/samtools/convert/samtools-convert.diff" }, "samtools/coverage": { "branch": "master", "git_sha": "2d20463181b1c38981a02e90d3084b5f9fa8d540", - "installed_by": ["modules"], + "installed_by": [ + "modules" + ], "patch": "modules/nf-core/samtools/coverage/samtools-coverage.diff" }, "samtools/fastq": { "branch": "master", "git_sha": "41dfa3f7c0ffabb96a6a813fe321c6d1cc5b6e46", - "installed_by": ["modules"] + "installed_by": [ + "modules" + ] }, "samtools/flagstat": { "branch": "master", "git_sha": "2d20463181b1c38981a02e90d3084b5f9fa8d540", - "installed_by": ["modules"] + "installed_by": [ + "modules" + ] }, "samtools/idxstats": { "branch": "master", "git_sha": "2d20463181b1c38981a02e90d3084b5f9fa8d540", - "installed_by": ["modules"] + "installed_by": [ + "modules" + ] }, "samtools/import": { "branch": "master", "git_sha": "2d20463181b1c38981a02e90d3084b5f9fa8d540", - "installed_by": ["modules"] + "installed_by": [ + "modules" + ] }, "samtools/index": { "branch": "master", "git_sha": "41dfa3f7c0ffabb96a6a813fe321c6d1cc5b6e46", - "installed_by": ["modules"] + "installed_by": [ + "modules" + ] }, "samtools/sormadup": { "branch": "master", "git_sha": "38f3b0200093498b70ac2d63a83eac5642e3c873", - "installed_by": ["modules"], + "installed_by": [ + "modules" + ], "patch": "modules/nf-core/samtools/sormadup/samtools-sormadup.diff" }, "samtools/sort": { "branch": "master", "git_sha": "b7800db9b069ed505db3f9d91b8c72faea9be17b", - "installed_by": ["modules"], + "installed_by": [ + "modules" + ], "patch": "modules/nf-core/samtools/sort/samtools-sort.diff" }, "samtools/stats": { "branch": "master", "git_sha": "2d20463181b1c38981a02e90d3084b5f9fa8d540", - "installed_by": ["modules"], + "installed_by": [ + "modules" + ], "patch": "modules/nf-core/samtools/stats/samtools-stats.diff" }, "snapaligner/align": { "branch": "master", "git_sha": "77bdd7e1047d2abe21ae8d89acc295ea553ecbae", - "installed_by": ["fastq_align_dna", "modules"], + "installed_by": [ + "fastq_align_dna", + "modules" + ], "patch": "modules/nf-core/snapaligner/align/snapaligner-align.diff" }, "star/align": { "branch": "master", "git_sha": "3c259f0ac1ed9ae3f6b835461d054edffc60120e", - "installed_by": ["modules"], + "installed_by": [ + "modules" + ], "patch": "modules/nf-core/star/align/star-align.diff" } } @@ -210,27 +288,37 @@ "bcl_demultiplex": { "branch": "master", "git_sha": "27cceb2eb8aa959d4a8819caab886386a59a3789", - "installed_by": ["subworkflows"] + "installed_by": [ + "subworkflows" + ] }, "fastq_align_dna": { "branch": "master", "git_sha": "a29f18660f5e3748d44d6f716241e70c942c065d", - "installed_by": ["subworkflows"] + "installed_by": [ + "subworkflows" + ] }, "utils_nextflow_pipeline": { "branch": "master", "git_sha": "56372688d8979092cafbe0c5c3895b491166ca1c", - "installed_by": ["subworkflows"] + "installed_by": [ + "subworkflows" + ] }, "utils_nfcore_pipeline": { "branch": "master", "git_sha": "1b6b9a3338d011367137808b49b923515080e3ba", - "installed_by": ["subworkflows"] + "installed_by": [ + "subworkflows" + ] }, "utils_nfschema_plugin": { "branch": "master", "git_sha": "2fd2cd6d0e7b273747f32e465fdc6bcc3ae0814e", - "installed_by": ["subworkflows"] + "installed_by": [ + "subworkflows" + ] } } } From 53e778ebceac18220fb78153e7566303873b9469 Mon Sep 17 00:00:00 2001 From: hwjeong Date: Fri, 22 Aug 2025 10:30:36 +0200 Subject: [PATCH 20/33] params.umi_group_strategy added --- nextflow.config | 3 ++ nextflow_schema.json | 45 ++++++++++------------------ subworkflows/local/consensus/main.nf | 18 ++++++----- 3 files changed, 29 insertions(+), 37 deletions(-) diff --git a/nextflow.config b/nextflow.config index cd44c534..02af618a 100644 --- a/nextflow.config +++ b/nextflow.config @@ -26,6 +26,9 @@ params { roi = null genelists = null + // UMI options + umi_group_strategy = 'adjacency' + // References genomes = [:] igenomes_base = '/references' diff --git a/nextflow_schema.json b/nextflow_schema.json index 53af1851..26aff03d 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -10,10 +10,7 @@ "type": "object", "fa_icon": "fas fa-terminal", "description": "Define where the pipeline should find input data and save output data.", - "required": [ - "input", - "outdir" - ], + "required": ["input", "outdir"], "properties": { "input": { "type": "string", @@ -54,24 +51,13 @@ "type": "string", "default": "bowtie2", "description": "Which aligner to use", - "enum": [ - "bowtie2", - "bwamem", - "bwamem2", - "dragmap", - "snap", - "star" - ] + "enum": ["bowtie2", "bwamem", "bwamem2", "dragmap", "snap", "star"] }, "markdup": { "type": "string", "default": "bamsormadup", "description": "Which alignment postprocessor to use", - "enum": [ - "bamsormadup", - "samtools", - "false" - ] + "enum": ["bamsormadup", "samtools", "false"] }, "umi_aware": { "type": "boolean", @@ -119,9 +105,7 @@ "description": "Directory containing gene list bed files for granular coverage analysis" } }, - "required": [ - "aligner" - ] + "required": ["aligner"] }, "institutional_config_options": { "title": "Institutional config options", @@ -196,14 +180,7 @@ "description": "Method used to save pipeline results to output directory.", "help_text": "The Nextflow `publishDir` option specifies which intermediate files should be saved to the output directory. This option tells the pipeline what method should be used to move these files. See [Nextflow docs](https://www.nextflow.io/docs/latest/process.html#publishdir) for details.", "fa_icon": "fas fa-copy", - "enum": [ - "symlink", - "rellink", - "link", - "copy", - "copyNoFollow", - "move" - ], + "enum": ["symlink", "rellink", "link", "copy", "copyNoFollow", "move"], "hidden": true }, "email_on_fail": { @@ -282,5 +259,13 @@ { "$ref": "#/$defs/generic_options" } - ] -} \ No newline at end of file + ], + "properties": { + "umi_group_strategy": { + "type": "string", + "description": "Strategy for Mapped Bam => Grouped BAM ('identity', 'edit', 'adjacency', 'paired')", + "default": "adjacency", + "hidden": true + } + } +} diff --git a/subworkflows/local/consensus/main.nf b/subworkflows/local/consensus/main.nf index 18223b4c..e1a3a985 100644 --- a/subworkflows/local/consensus/main.nf +++ b/subworkflows/local/consensus/main.nf @@ -15,7 +15,7 @@ include { FASTQ_ALIGN_DNA } from '../../../subworkflows/ workflow CONSENSUS { take: ch_umi_fastq // channel: [meta_with_readgroup, fastq] for SE/PE/duplex samples - + main: def ch_versions = Channel.empty() def ch_ubam = Channel.empty() @@ -92,16 +92,20 @@ workflow CONSENSUS { FGBIO_ZIPPERBAMS(ch_zipperbam) ch_versions = ch_versions.mix(FGBIO_ZIPPERBAMS.out.versions) - - + + // 1.3: Mapped BAM => Grouped BAM - // 1.3: Mapped BAM => Grouped BAM - def ch_strategy = Channel.value( (params.umi_group_strategy ?: 'Adjacency') as String ) + def valid_strategies = ['identity', 'edit', 'adjacency', 'paired'] + def umi_strategy = (params.umi_group_strategy ?: 'adjacency').toLowerCase() + if ( !valid_strategies.contains(umi_strategy) ) { + exit 1, "Invalid value for --umi_group_strategy: '${params.umi_group_strategy}'. Allowed values: ${valid_strategies.join(', ')}" + } + def ch_strategy = Channel.value(umi_strategy) FGBIO_GROUPREADSBYUMI( - FGBIO_ZIPPERBAMS.out.bam, - ch_strategy + FGBIO_ZIPPERBAMS.out.bam, + ch_strategy ) ch_versions = ch_versions.mix(FGBIO_GROUPREADSBYUMI.out.versions) From 2fe0d460cf5adb5f981ef19a1db0dea530f15cf9 Mon Sep 17 00:00:00 2001 From: hwjeong Date: Fri, 22 Aug 2025 10:50:27 +0200 Subject: [PATCH 21/33] param.umi_group_strategy update --- nextflow_schema.json | 17 ++++++++--------- subworkflows/local/consensus/main.nf | 9 +++------ 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/nextflow_schema.json b/nextflow_schema.json index 26aff03d..3d0a61b2 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -47,6 +47,13 @@ "description": "", "default": "", "properties": { + "umi_group_strategy": { + "type": "string", + "description": "Strategy for Mapped Bam => Grouped BAM ('identity', 'edit', 'adjacency', 'paired')", + "default": "adjacency", + "hidden": true, + "enum": ["identity", "edit", "adjacency", "paired"] + }, "aligner": { "type": "string", "default": "bowtie2", @@ -259,13 +266,5 @@ { "$ref": "#/$defs/generic_options" } - ], - "properties": { - "umi_group_strategy": { - "type": "string", - "description": "Strategy for Mapped Bam => Grouped BAM ('identity', 'edit', 'adjacency', 'paired')", - "default": "adjacency", - "hidden": true - } - } + ] } diff --git a/subworkflows/local/consensus/main.nf b/subworkflows/local/consensus/main.nf index e1a3a985..2554c423 100644 --- a/subworkflows/local/consensus/main.nf +++ b/subworkflows/local/consensus/main.nf @@ -96,12 +96,8 @@ workflow CONSENSUS { // 1.3: Mapped BAM => Grouped BAM - def valid_strategies = ['identity', 'edit', 'adjacency', 'paired'] - def umi_strategy = (params.umi_group_strategy ?: 'adjacency').toLowerCase() - if ( !valid_strategies.contains(umi_strategy) ) { - exit 1, "Invalid value for --umi_group_strategy: '${params.umi_group_strategy}'. Allowed values: ${valid_strategies.join(', ')}" - } - def ch_strategy = Channel.value(umi_strategy) + + def ch_strategy = Channel.value(params.umi_group_strategy) FGBIO_GROUPREADSBYUMI( FGBIO_ZIPPERBAMS.out.bam, @@ -111,6 +107,7 @@ workflow CONSENSUS { ch_versions = ch_versions.mix(FGBIO_GROUPREADSBYUMI.out.versions) def ch_grouped_bam = FGBIO_GROUPREADSBYUMI.out.bam + emit: ubam = ch_ubam consensus_bam = FGBIO_ZIPPERBAMS.out.bam From 02664963cd0bc8410f59bc8cda8cbd008e313c91 Mon Sep 17 00:00:00 2001 From: hwjeong Date: Fri, 22 Aug 2025 11:30:52 +0200 Subject: [PATCH 22/33] 2(b).1 finished --- modules.json | 5 +-- .../fgbio-filterconsensusreads.diff | 20 ++++++++++++ .../fgbio/filterconsensusreads/main.nf | 3 +- nextflow.config | 7 +++- nextflow_schema.json | 30 +++++++++++++++++ subworkflows/local/consensus/main.nf | 32 +++++++++++++++++++ 6 files changed, 92 insertions(+), 5 deletions(-) create mode 100644 modules/nf-core/fgbio/filterconsensusreads/fgbio-filterconsensusreads.diff diff --git a/modules.json b/modules.json index fd3bc419..223ce972 100644 --- a/modules.json +++ b/modules.json @@ -111,7 +111,8 @@ "git_sha": "41dfa3f7c0ffabb96a6a813fe321c6d1cc5b6e46", "installed_by": [ "modules" - ] + ], + "patch": "modules/nf-core/fgbio/filterconsensusreads/fgbio-filterconsensusreads.diff" }, "fgbio/groupreadsbyumi": { "branch": "master", @@ -324,4 +325,4 @@ } } } -} +} \ No newline at end of file diff --git a/modules/nf-core/fgbio/filterconsensusreads/fgbio-filterconsensusreads.diff b/modules/nf-core/fgbio/filterconsensusreads/fgbio-filterconsensusreads.diff new file mode 100644 index 00000000..9d55d91e --- /dev/null +++ b/modules/nf-core/fgbio/filterconsensusreads/fgbio-filterconsensusreads.diff @@ -0,0 +1,20 @@ +Changes in component 'nf-core/fgbio/filterconsensusreads' +'modules/nf-core/fgbio/filterconsensusreads/environment.yml' is unchanged +Changes in 'fgbio/filterconsensusreads/main.nf': +--- modules/nf-core/fgbio/filterconsensusreads/main.nf ++++ modules/nf-core/fgbio/filterconsensusreads/main.nf +@@ -8,8 +8,7 @@ + 'community.wave.seqera.io/library/fgbio:2.5.21--368dab1b4f308243' }" + + input: +- tuple val(meta), path(bam) +- tuple val(meta2), path(fasta) ++ tuple val(meta), path(bam), path(fasta) + val(min_reads) + val(min_baseq) + val(max_base_error_rate) + +'modules/nf-core/fgbio/filterconsensusreads/meta.yml' is unchanged +'modules/nf-core/fgbio/filterconsensusreads/tests/main.nf.test' is unchanged +'modules/nf-core/fgbio/filterconsensusreads/tests/main.nf.test.snap' is unchanged +************************************************************ diff --git a/modules/nf-core/fgbio/filterconsensusreads/main.nf b/modules/nf-core/fgbio/filterconsensusreads/main.nf index 717a2587..007887e2 100644 --- a/modules/nf-core/fgbio/filterconsensusreads/main.nf +++ b/modules/nf-core/fgbio/filterconsensusreads/main.nf @@ -8,8 +8,7 @@ process FGBIO_FILTERCONSENSUSREADS { 'community.wave.seqera.io/library/fgbio:2.5.21--368dab1b4f308243' }" input: - tuple val(meta), path(bam) - tuple val(meta2), path(fasta) + tuple val(meta), path(bam), path(fasta) val(min_reads) val(min_baseq) val(max_base_error_rate) diff --git a/nextflow.config b/nextflow.config index 02af618a..cabaec67 100644 --- a/nextflow.config +++ b/nextflow.config @@ -27,7 +27,12 @@ params { genelists = null // UMI options - umi_group_strategy = 'adjacency' + umi_group_strategy = 'adjacency' + callmolecularconsensusreads_min_reads = 3 + callmolecularconsensusreads_min_baseq = 20 + filterconsensusreads_min_reads = 3 + filterconsensusreads_min_baseq = 45 + filterconsensusreads_min_base_error_rate = 0.2 // References genomes = [:] diff --git a/nextflow_schema.json b/nextflow_schema.json index 3d0a61b2..45322b3a 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -54,6 +54,36 @@ "hidden": true, "enum": ["identity", "edit", "adjacency", "paired"] }, + "callmolecularconsensusreads_min_reads": { + "type": "integer", + "default": 3, + "description": "Minimum reads for callmolecularconsensusreads", + "hidden": true + }, + "callmolecularconsensusreads_min_baseq": { + "type": "integer", + "default": 20, + "description": "Minimum base quality for callmolecularconsensusreads", + "hidden": true + }, + "filterconsensusreads_min_reads": { + "type": "integer", + "default": 3, + "hidden": true, + "description": "Minimum reads for filterconsensusreads" + }, + "filterconsensusreads_min_baseq": { + "type": "integer", + "default": 45, + "hidden": true, + "description": "Minimum base quality for filterconsensusreads" + }, + "filterconsensusreads_min_base_error_rate": { + "type": "number", + "default": 0.2, + "hidden": true, + "description": "Minimum base error rate for filterconsensusreads" + }, "aligner": { "type": "string", "default": "bowtie2", diff --git a/subworkflows/local/consensus/main.nf b/subworkflows/local/consensus/main.nf index 2554c423..be111317 100644 --- a/subworkflows/local/consensus/main.nf +++ b/subworkflows/local/consensus/main.nf @@ -107,10 +107,42 @@ workflow CONSENSUS { ch_versions = ch_versions.mix(FGBIO_GROUPREADSBYUMI.out.versions) def ch_grouped_bam = FGBIO_GROUPREADSBYUMI.out.bam + // 2(b).1: GroupedBam -> Filtered Consensus uBam + def call_min_reads = Channel.value(params.callmolecularconsensusreads_min_reads) + def call_min_baseq = Channel.value(params.callmolecularconsensusreads_min_baseq) + + FGBIO_CALLMOLECULARCONSENSUSREADS( + ch_grouped_bam, + call_min_reads, + call_min_baseq + ) + + ch_versions = ch_versions.mix(FGBIO_CALLMOLECULARCONSENSUSREADS.out.versions) + + def ch_input_filterconsensusreads = FGBIO_CALLMOLECULARCONSENSUSREADS.out.bam.map {meta, bam -> + def fasta = file(meta.genome_data.fasta, checkIfExists: true) + tuple(meta, bam, fasta) + } + + def filter_min_reads = Channel.value(params.filterconsensusreads_min_reads) + def filter_min_baseq = Channel.value(params.filterconsensusreads_min_baseq) + def filter_min_base_error_rate = Channel.value(params.filterconsensusreads_min_base_error_rate) + + FGBIO_FILTERCONSENSUSREADS( + ch_input_filterconsensusreads, + filter_min_reads, + filter_min_baseq, + filter_min_base_error_rate + ) + + ch_versions = ch_versions.mix(FGBIO_FILTERCONSENSUSREADS.out.versions) + + ch_filtered_uBam = FGBIO_FILTERCONSENSUSREADS.out.bam emit: ubam = ch_ubam consensus_bam = FGBIO_ZIPPERBAMS.out.bam grouped_bam = ch_grouped_bam + filtered_ubam = ch_filtered_uBam versions = ch_versions } From be65aa71173b74f7dfbf302f3d2659e22e223798 Mon Sep 17 00:00:00 2001 From: Dhamar Date: Fri, 22 Aug 2025 12:49:34 +0200 Subject: [PATCH 23/33] Step 2(b).2: Consensus Filtered uBam -> Consensus Mapped & Filtered BAM --- subworkflows/local/consensus/main.nf | 92 ++++++++++++++++++++-------- 1 file changed, 67 insertions(+), 25 deletions(-) diff --git a/subworkflows/local/consensus/main.nf b/subworkflows/local/consensus/main.nf index be111317..c3b11f6d 100644 --- a/subworkflows/local/consensus/main.nf +++ b/subworkflows/local/consensus/main.nf @@ -1,16 +1,19 @@ #!/usr/bin/env nextflow -include { FGBIO_COPYUMIFROMREADNAME } from '../../../modules/nf-core/fgbio/copyumifromreadname/main' -include { FGBIO_CALLMOLECULARCONSENSUSREADS } from '../../../modules/nf-core/fgbio/callmolecularconsensusreads/main' -include { FGBIO_FASTQTOBAM as FASTQTOBAM_READNAME } from '../../../modules/nf-core/fgbio/fastqtobam/main' -include { FGBIO_FASTQTOBAM as FASTQTOBAM_SEQ } from '../../../modules/nf-core/fgbio/fastqtobam/main' -include { FGBIO_FILTERCONSENSUSREADS } from '../../../modules/nf-core/fgbio/filterconsensusreads/main' -include { FGBIO_GROUPREADSBYUMI } from '../../../modules/nf-core/fgbio/groupreadsbyumi/main' -include { FGBIO_SORTBAM } from '../../../modules/nf-core/fgbio/sortbam/main' -include { FGBIO_ZIPPERBAMS } from '../../../modules/nf-core/fgbio/zipperbams/main' -include { SAMTOOLS_FASTQ } from '../../../modules/nf-core/samtools/fastq/main' -include { SAMTOOLS_INDEX } from '../../../modules/nf-core/samtools/index/main' -include { FASTQ_ALIGN_DNA } from '../../../subworkflows/nf-core/fastq_align_dna/main' +include { FGBIO_COPYUMIFROMREADNAME } from '../../../modules/nf-core/fgbio/copyumifromreadname/main' +include { FGBIO_CALLMOLECULARCONSENSUSREADS } from '../../../modules/nf-core/fgbio/callmolecularconsensusreads/main' +include { FGBIO_FASTQTOBAM as FASTQTOBAM_READNAME } from '../../../modules/nf-core/fgbio/fastqtobam/main' +include { FGBIO_FASTQTOBAM as FASTQTOBAM_SEQ } from '../../../modules/nf-core/fgbio/fastqtobam/main' +include { FGBIO_FILTERCONSENSUSREADS } from '../../../modules/nf-core/fgbio/filterconsensusreads/main' +include { FGBIO_GROUPREADSBYUMI } from '../../../modules/nf-core/fgbio/groupreadsbyumi/main' +include { FGBIO_SORTBAM } from '../../../modules/nf-core/fgbio/sortbam/main' +include { FGBIO_ZIPPERBAMS as FGBIO_ZIPPERBAMS_RAW } from '../../../modules/nf-core/fgbio/zipperbams/main' +include { FGBIO_ZIPPERBAMS as FGBIO_ZIPPERBAMS_CONS } from '../../../modules/nf-core/fgbio/zipperbams/main' +include { SAMTOOLS_FASTQ as SAMTOOLS_FASTQ_RAW } from '../../../modules/nf-core/samtools/fastq/main' +include { SAMTOOLS_FASTQ as SAMTOOLS_FASTQ_CONS } from '../../../modules/nf-core/samtools/fastq/main' +include { SAMTOOLS_INDEX } from '../../../modules/nf-core/samtools/index/main' +include { FASTQ_ALIGN_DNA as FASTQ_ALIGN_DNA_RAW } from '../../../subworkflows/nf-core/fastq_align_dna/main' +include { FASTQ_ALIGN_DNA as FASTQ_ALIGN_DNA_CONS } from '../../../subworkflows/nf-core/fastq_align_dna/main' workflow CONSENSUS { take: @@ -59,11 +62,11 @@ workflow CONSENSUS { // 1.2: uBAM => Mapped BAM - SAMTOOLS_FASTQ(ch_ubam, true) + SAMTOOLS_FASTQ_RAW(ch_ubam, true) - ch_versions = ch_versions.mix(SAMTOOLS_FASTQ.out.versions) + ch_versions = ch_versions.mix(SAMTOOLS_FASTQ_RAW.out.versions) - def ch_reads_aligner_index_fasta = SAMTOOLS_FASTQ.out.interleaved.map { meta, reads -> + def ch_reads_aligner_index_fasta = SAMTOOLS_FASTQ_RAW.out.interleaved.map { meta, reads -> def gd = (meta.genome_data instanceof Map) ? meta.genome_data : [:] def alg = (meta.aligner ?: 'bwamem') def fasta = file(gd.fasta, checkIfExists: true) @@ -71,10 +74,10 @@ workflow CONSENSUS { tuple(meta, reads, alg, index, fasta) } - FASTQ_ALIGN_DNA(ch_reads_aligner_index_fasta, false) - ch_versions = ch_versions.mix(FASTQ_ALIGN_DNA.out.versions) + FASTQ_ALIGN_DNA_RAW(ch_reads_aligner_index_fasta, false) + ch_versions = ch_versions.mix(FASTQ_ALIGN_DNA_RAW.out.versions) - def ch_mapped_bam = FASTQ_ALIGN_DNA.out.bam + def ch_mapped_bam = FASTQ_ALIGN_DNA_RAW.out.bam def ch_fasta_by_meta = ch_reads_aligner_index_fasta.map { meta, _r, _a, _i, fasta -> tuple(meta, fasta) } @@ -89,9 +92,9 @@ workflow CONSENSUS { .map { meta, ubam, mapped_bam, fasta, dict -> tuple(meta, ubam, mapped_bam, fasta, dict) } .set { ch_zipperbam } - FGBIO_ZIPPERBAMS(ch_zipperbam) + FGBIO_ZIPPERBAMS_RAW(ch_zipperbam) - ch_versions = ch_versions.mix(FGBIO_ZIPPERBAMS.out.versions) + ch_versions = ch_versions.mix(FGBIO_ZIPPERBAMS_RAW.out.versions) // 1.3: Mapped BAM => Grouped BAM @@ -100,7 +103,7 @@ workflow CONSENSUS { def ch_strategy = Channel.value(params.umi_group_strategy) FGBIO_GROUPREADSBYUMI( - FGBIO_ZIPPERBAMS.out.bam, + FGBIO_ZIPPERBAMS_RAW.out.bam, ch_strategy ) @@ -139,10 +142,49 @@ workflow CONSENSUS { ch_filtered_uBam = FGBIO_FILTERCONSENSUSREADS.out.bam + // 2(b).2: Consensus Filtered uBam -> Consensus Mapped & Filtered BAM + + SAMTOOLS_FASTQ_CONS(ch_filtered_uBam, true) + ch_versions = ch_versions.mix(SAMTOOLS_FASTQ_CONS.out.versions) + + def ch_cons_reads_aligner_index_fasta = SAMTOOLS_FASTQ_CONS.out.interleaved.map { meta, reads -> + def gd = (meta.genome_data instanceof Map) ? meta.genome_data : [:] + def alg = (meta.aligner ?: 'bwamem') + def fasta = file(gd.fasta, checkIfExists: true) + def index = file(gd[alg], checkIfExists: true) + tuple(meta, reads, alg, index, fasta) + } + + FASTQ_ALIGN_DNA_CONS(ch_cons_reads_aligner_index_fasta, false) + ch_versions = ch_versions.mix(FASTQ_ALIGN_DNA_CONS.out.versions) + + def ch_cons_mapped_bam = FASTQ_ALIGN_DNA_CONS.out.bam + def ch_cons_fasta_by_meta = ch_cons_reads_aligner_index_fasta.map { meta, _r, _a, _i, fasta -> tuple(meta, fasta) } + def ch_cons_dict_by_meta = ch_cons_reads_aligner_index_fasta.map { meta, _r, _a, _i, _fasta -> + def dict = file(meta.genome_data.dict, checkIfExists: true) + tuple(meta, dict) + } + + ch_filtered_uBam + .join(ch_cons_mapped_bam, by: 0) + .join(ch_cons_fasta_by_meta, by: 0) + .join(ch_cons_dict_by_meta, by: 0) + .map { meta, ubam, mapped_bam, fasta, dict -> tuple(meta, ubam, mapped_bam, fasta, dict) } + .set { ch_cons_zipperbam } + + FGBIO_ZIPPERBAMS_CONS(ch_cons_zipperbam) + ch_versions = ch_versions.mix(FGBIO_ZIPPERBAMS_CONS.out.versions) + + FGBIO_SORTBAM(FGBIO_ZIPPERBAMS_CONS.out.bam) + ch_versions = ch_versions.mix(FGBIO_SORTBAM.out.versions) + + def ch_consensus_filtered_bam = FGBIO_SORTBAM.out.bam + + emit: - ubam = ch_ubam - consensus_bam = FGBIO_ZIPPERBAMS.out.bam - grouped_bam = ch_grouped_bam - filtered_ubam = ch_filtered_uBam - versions = ch_versions + ubam = ch_ubam + consensus_bam = ch_consensus_filtered_bam + grouped_bam = ch_grouped_bam + filtered_ubam = ch_filtered_uBam + versions = ch_versions } From 8246e399c0a148146d6634c5a231e3a4ecec2815 Mon Sep 17 00:00:00 2001 From: hwjeong Date: Sat, 23 Aug 2025 15:56:50 +0200 Subject: [PATCH 24/33] nf-test script - 1st Attempt --- .../subworkflows/local/consensus/main.nf.test | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 tests/subworkflows/local/consensus/main.nf.test diff --git a/tests/subworkflows/local/consensus/main.nf.test b/tests/subworkflows/local/consensus/main.nf.test new file mode 100644 index 00000000..bef7c8d8 --- /dev/null +++ b/tests/subworkflows/local/consensus/main.nf.test @@ -0,0 +1,75 @@ +nextflow_workflow { + name "Test Subworkflow CONSENSUS" + script "subworkflows/local/consensus/main.nf" + workflow "CONSENSUS" + + tag "subworkflows" + tag "consensus" + tag "fgbio" + tag "umi" + tag "bwamem" + + test("test_consensus") { + + when { + workflow { + """ + input[0] = Channel.of([ + [ + [ + id : 'umi_sample1', + samplename: 'umi_test1', + library : 'test', + organism : 'Homo sapiens', + umi_type : 'seq', + tag : 'WES', + aligner : 'bwamem', + genome_data: [ + fasta : params.genomes.GRCh38.fasta, + dict : params.genomes.GRCh38.dict, + bwamem: params.genomes.GRCh38.bwamem + ] + ], + file('https://raw.githubusercontent.com/nf-core/test-datasets/modules/data/genomics/homo_sapiens/illumina/fastq/test.umi_1.fastq.gz'), + file('https://raw.githubusercontent.com/nf-core/test-datasets/modules/data/genomics/homo_sapiens/illumina/fastq/test.umi_2.fastq.gz') + ], + [ + [ + id : 'umi_sample2', + samplename: 'umi_test2', + library : 'test', + organism : 'Homo sapiens', + umi_type : 'seq', + tag : 'WES', + aligner : 'bwamem', + genome_data: [ + fasta : params.genomes.GRCh38.fasta, + dict : params.genomes.GRCh38.dict, + bwamem: params.genomes.GRCh38.bwamem + ] + ], + file('https://raw.githubusercontent.com/nf-core/test-datasets/modules/data/genomics/homo_sapiens/illumina/fastq/test2.umi_1.fastq.gz'), + file('https://raw.githubusercontent.com/nf-core/test-datasets/modules/data/genomics/homo_sapiens/illumina/fastq/test2.umi_2.fastq.gz') + + ] + ]) + """ + } + } + + then { + assertAll( + { + assert workflow.success + assert snapshot( + workflow.out.ubam.collect { it[1].name }, + workflow.out.grouped_bam.collect { it[1].name }, + workflow.out.filtered_ubam.collect { it[1].name }, + workflow.out.consensus_bam.collect { it[1].name }, + workflow.out.versions + ).match() + } + ) + } + } +} From 2f4ad6fbeca48d5bdb405300090033c41bb8eebc Mon Sep 17 00:00:00 2001 From: hwjeong Date: Mon, 25 Aug 2025 09:51:14 +0200 Subject: [PATCH 25/33] nf-test script 2nd attempt --- tests/config/nf-test.config | 4 +++ .../subworkflows/local/consensus/main.nf.test | 31 +++++++++++++++---- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/tests/config/nf-test.config b/tests/config/nf-test.config index 68e597f9..54b7070e 100644 --- a/tests/config/nf-test.config +++ b/tests/config/nf-test.config @@ -14,3 +14,7 @@ aws { connectionTimeout = 60000 } } + +plugins = [ + "nft-bam" +] diff --git a/tests/subworkflows/local/consensus/main.nf.test b/tests/subworkflows/local/consensus/main.nf.test index bef7c8d8..bb9e16f1 100644 --- a/tests/subworkflows/local/consensus/main.nf.test +++ b/tests/subworkflows/local/consensus/main.nf.test @@ -1,3 +1,6 @@ +import nf.test.NFTest +import nf.test.bam.BamFile + nextflow_workflow { name "Test Subworkflow CONSENSUS" script "subworkflows/local/consensus/main.nf" @@ -9,7 +12,7 @@ nextflow_workflow { tag "umi" tag "bwamem" - test("test_consensus") { + test("test_consensus_reads_md5") { when { workflow { @@ -50,7 +53,6 @@ nextflow_workflow { ], file('https://raw.githubusercontent.com/nf-core/test-datasets/modules/data/genomics/homo_sapiens/illumina/fastq/test2.umi_1.fastq.gz'), file('https://raw.githubusercontent.com/nf-core/test-datasets/modules/data/genomics/homo_sapiens/illumina/fastq/test2.umi_2.fastq.gz') - ] ]) """ @@ -61,11 +63,28 @@ nextflow_workflow { assertAll( { assert workflow.success + + def ubam_md5s = workflow.out.ubam.collect { + new BamFile(it[1]).getReadsMD5() + } + + def grouped_md5s = workflow.out.grouped_bam.collect { + new BamFile(it[1]).getReadsMD5() + } + + def filtered_md5s = workflow.out.filtered_ubam.collect { + new BamFile(it[1]).getReadsMD5() + } + + def consensus_md5s = workflow.out.consensus_bam.collect { + new BamFile(it[1]).getReadsMD5() + } + assert snapshot( - workflow.out.ubam.collect { it[1].name }, - workflow.out.grouped_bam.collect { it[1].name }, - workflow.out.filtered_ubam.collect { it[1].name }, - workflow.out.consensus_bam.collect { it[1].name }, + ubam_md5s, + grouped_md5s, + filtered_md5s, + consensus_md5s, workflow.out.versions ).match() } From 06f2da837bd698bda294cc9f81f5ed916086acf9 Mon Sep 17 00:00:00 2001 From: hwjeong Date: Mon, 25 Aug 2025 11:11:38 +0200 Subject: [PATCH 26/33] nf-test with snapshot --- nf-test.config | 4 +- .../subworkflows/local/consensus/main.nf.test | 90 +++++-------------- .../local/consensus/main.nf.test.snap | 26 ++++++ 3 files changed, 53 insertions(+), 67 deletions(-) create mode 100644 tests/subworkflows/local/consensus/main.nf.test.snap diff --git a/nf-test.config b/nf-test.config index 0688f302..8829a169 100644 --- a/nf-test.config +++ b/nf-test.config @@ -1,9 +1,11 @@ config { + plugins { + load "nft-bam@0.6.0" + } testsDir "tests" workDir ".nf-test" configFile "tests/config/nf-test.config" profile "docker" options "-dump-channels" - } diff --git a/tests/subworkflows/local/consensus/main.nf.test b/tests/subworkflows/local/consensus/main.nf.test index bb9e16f1..1a759193 100644 --- a/tests/subworkflows/local/consensus/main.nf.test +++ b/tests/subworkflows/local/consensus/main.nf.test @@ -1,6 +1,3 @@ -import nf.test.NFTest -import nf.test.bam.BamFile - nextflow_workflow { name "Test Subworkflow CONSENSUS" script "subworkflows/local/consensus/main.nf" @@ -19,76 +16,37 @@ nextflow_workflow { """ input[0] = Channel.of([ [ - [ - id : 'umi_sample1', - samplename: 'umi_test1', - library : 'test', - organism : 'Homo sapiens', - umi_type : 'seq', - tag : 'WES', - aligner : 'bwamem', - genome_data: [ - fasta : params.genomes.GRCh38.fasta, - dict : params.genomes.GRCh38.dict, - bwamem: params.genomes.GRCh38.bwamem - ] - ], - file('https://raw.githubusercontent.com/nf-core/test-datasets/modules/data/genomics/homo_sapiens/illumina/fastq/test.umi_1.fastq.gz'), - file('https://raw.githubusercontent.com/nf-core/test-datasets/modules/data/genomics/homo_sapiens/illumina/fastq/test.umi_2.fastq.gz') + id : 'umi_sample1', + samplename: 'umi_test1', + library : 'test', + organism : 'Homo sapiens', + umi_type : 'seq', + tag : 'WES', + aligner : 'bwamem', + genome_data: [ + fasta : params.genomes.GRCh38.fasta, + dict : params.genomes.GRCh38.dict, + bwamem: params.genomes.GRCh38.bwamem + ] ], - [ - [ - id : 'umi_sample2', - samplename: 'umi_test2', - library : 'test', - organism : 'Homo sapiens', - umi_type : 'seq', - tag : 'WES', - aligner : 'bwamem', - genome_data: [ - fasta : params.genomes.GRCh38.fasta, - dict : params.genomes.GRCh38.dict, - bwamem: params.genomes.GRCh38.bwamem - ] - ], - file('https://raw.githubusercontent.com/nf-core/test-datasets/modules/data/genomics/homo_sapiens/illumina/fastq/test2.umi_1.fastq.gz'), - file('https://raw.githubusercontent.com/nf-core/test-datasets/modules/data/genomics/homo_sapiens/illumina/fastq/test2.umi_2.fastq.gz') - ] + file('https://raw.githubusercontent.com/nf-core/test-datasets/modules/data/genomics/homo_sapiens/illumina/fastq/test.umi_1.fastq.gz'), + file('https://raw.githubusercontent.com/nf-core/test-datasets/modules/data/genomics/homo_sapiens/illumina/fastq/test.umi_2.fastq.gz') ]) """ } } then { - assertAll( - { - assert workflow.success - - def ubam_md5s = workflow.out.ubam.collect { - new BamFile(it[1]).getReadsMD5() - } - - def grouped_md5s = workflow.out.grouped_bam.collect { - new BamFile(it[1]).getReadsMD5() - } - - def filtered_md5s = workflow.out.filtered_ubam.collect { - new BamFile(it[1]).getReadsMD5() - } - - def consensus_md5s = workflow.out.consensus_bam.collect { - new BamFile(it[1]).getReadsMD5() - } - - assert snapshot( - ubam_md5s, - grouped_md5s, - filtered_md5s, - consensus_md5s, - workflow.out.versions - ).match() - } - ) + assertAll { + assert workflow.success + assert snapshot( + workflow.out.ubam.collect { bam(it[1]).getReadsMD5() }, + workflow.out.grouped_bam.collect { bam(it[1]).getReadsMD5() }, + workflow.out.filtered_ubam.collect { bam(it[1]).getReadsMD5() }, + workflow.out.consensus_bam.collect { bam(it[1]).getReadsMD5() }, + workflow.out.versions + ).match() + } } } } diff --git a/tests/subworkflows/local/consensus/main.nf.test.snap b/tests/subworkflows/local/consensus/main.nf.test.snap new file mode 100644 index 00000000..671d9784 --- /dev/null +++ b/tests/subworkflows/local/consensus/main.nf.test.snap @@ -0,0 +1,26 @@ +{ + "test_consensus_reads_md5": { + "content": [ + [ + + ], + [ + + ], + [ + + ], + [ + + ], + [ + "versions.yml:md5,92ec3e7cd231a6503321464eb83e6955" + ] + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.6" + }, + "timestamp": "2025-08-25T11:10:48.133975221" + } +} \ No newline at end of file From 3e34992244daa9dc3ef121d4b424a9dbaad80dd5 Mon Sep 17 00:00:00 2001 From: hwjeong Date: Mon, 25 Aug 2025 20:35:27 +0200 Subject: [PATCH 27/33] nf-test with correct snapshot --- modules.json | 169 +++++------------- .../fgbio-copyumifromreadname.diff | 22 --- .../nf-core/fgbio/copyumifromreadname/main.nf | 4 +- .../fgbio/fastqtobam/fgbio-fastqtobam.diff | 25 --- modules/nf-core/fgbio/fastqtobam/main.nf | 4 +- nextflow.config | 12 ++ .../local/consensus/main.nf.test.snap | 22 ++- 7 files changed, 74 insertions(+), 184 deletions(-) delete mode 100644 modules/nf-core/fgbio/copyumifromreadname/fgbio-copyumifromreadname.diff delete mode 100644 modules/nf-core/fgbio/fastqtobam/fgbio-fastqtobam.diff diff --git a/modules.json b/modules.json index 223ce972..adcf495d 100644 --- a/modules.json +++ b/modules.json @@ -8,278 +8,203 @@ "bcl2fastq": { "branch": "master", "git_sha": "05954dab2ff481bcb999f24455da29a5828af08d", - "installed_by": [ - "bcl_demultiplex" - ] + "installed_by": ["bcl_demultiplex"] }, "bclconvert": { "branch": "master", "git_sha": "27cceb2eb8aa959d4a8819caab886386a59a3789", - "installed_by": [ - "bcl_demultiplex", - "modules" - ] + "installed_by": ["bcl_demultiplex", "modules"] }, "biobambam/bamsormadup": { "branch": "master", "git_sha": "666652151335353eef2fcd58880bcef5bc2928e1", - "installed_by": [ - "modules" - ], + "installed_by": ["modules"], "patch": "modules/nf-core/biobambam/bamsormadup/biobambam-bamsormadup.diff" }, "bowtie2/align": { "branch": "master", "git_sha": "8864afe586537bf562eac7b83349c26207f3cb4d", - "installed_by": [ - "fastq_align_dna", - "modules" - ], + "installed_by": ["fastq_align_dna", "modules"], "patch": "modules/nf-core/bowtie2/align/bowtie2-align.diff" }, "bwa/mem": { "branch": "master", "git_sha": "a29f18660f5e3748d44d6f716241e70c942c065d", - "installed_by": [ - "fastq_align_dna" - ], + "installed_by": ["fastq_align_dna"], "patch": "modules/nf-core/bwa/mem/bwa-mem.diff" }, "bwamem2/mem": { "branch": "master", "git_sha": "a29f18660f5e3748d44d6f716241e70c942c065d", - "installed_by": [ - "fastq_align_dna" - ], + "installed_by": ["fastq_align_dna"], "patch": "modules/nf-core/bwamem2/mem/bwamem2-mem.diff" }, "dragmap/align": { "branch": "master", "git_sha": "8864afe586537bf562eac7b83349c26207f3cb4d", - "installed_by": [ - "fastq_align_dna" - ], + "installed_by": ["fastq_align_dna"], "patch": "modules/nf-core/dragmap/align/dragmap-align.diff" }, "fastp": { "branch": "master", "git_sha": "666652151335353eef2fcd58880bcef5bc2928e1", - "installed_by": [ - "modules" - ], + "installed_by": ["modules"], "patch": "modules/nf-core/fastp/fastp.diff" }, "fgbio/callduplexconsensusreads": { "branch": "master", "git_sha": "41dfa3f7c0ffabb96a6a813fe321c6d1cc5b6e46", - "installed_by": [ - "modules" - ] + "installed_by": ["modules"] }, "fgbio/callmolecularconsensusreads": { "branch": "master", "git_sha": "41dfa3f7c0ffabb96a6a813fe321c6d1cc5b6e46", - "installed_by": [ - "modules" - ] + "installed_by": ["modules"] }, "fgbio/collectduplexseqmetrics": { "branch": "master", "git_sha": "41dfa3f7c0ffabb96a6a813fe321c6d1cc5b6e46", - "installed_by": [ - "modules" - ] + "installed_by": ["modules"] }, "fgbio/copyumifromreadname": { "branch": "master", "git_sha": "47dbfc0fbcd8e4e3b73d843f4659069441ca8692", - "installed_by": [ - "modules" - ], + "installed_by": ["modules"], "patch": "modules/nf-core/fgbio/copyumifromreadname/fgbio-copyumifromreadname.diff" }, "fgbio/fastqtobam": { "branch": "master", "git_sha": "41dfa3f7c0ffabb96a6a813fe321c6d1cc5b6e46", - "installed_by": [ - "modules" - ], + "installed_by": ["modules"], "patch": "modules/nf-core/fgbio/fastqtobam/fgbio-fastqtobam.diff" }, "fgbio/filterconsensusreads": { "branch": "master", "git_sha": "41dfa3f7c0ffabb96a6a813fe321c6d1cc5b6e46", - "installed_by": [ - "modules" - ], + "installed_by": ["modules"], "patch": "modules/nf-core/fgbio/filterconsensusreads/fgbio-filterconsensusreads.diff" }, "fgbio/groupreadsbyumi": { "branch": "master", "git_sha": "41dfa3f7c0ffabb96a6a813fe321c6d1cc5b6e46", - "installed_by": [ - "modules" - ] + "installed_by": ["modules"] }, "fgbio/sortbam": { "branch": "master", "git_sha": "41dfa3f7c0ffabb96a6a813fe321c6d1cc5b6e46", - "installed_by": [ - "modules" - ] + "installed_by": ["modules"] }, "fgbio/zipperbams": { "branch": "master", "git_sha": "41dfa3f7c0ffabb96a6a813fe321c6d1cc5b6e46", - "installed_by": [ - "modules" - ], + "installed_by": ["modules"], "patch": "modules/nf-core/fgbio/zipperbams/fgbio-zipperbams.diff" }, "md5sum": { "branch": "master", "git_sha": "666652151335353eef2fcd58880bcef5bc2928e1", - "installed_by": [ - "modules" - ] + "installed_by": ["modules"] }, "mosdepth": { "branch": "master", "git_sha": "666652151335353eef2fcd58880bcef5bc2928e1", - "installed_by": [ - "modules" - ], + "installed_by": ["modules"], "patch": "modules/nf-core/mosdepth/mosdepth.diff" }, "multiqc": { "branch": "master", "git_sha": "471cf3ca1617271b9b6fea09ea2ebdee78b874de", - "installed_by": [ - "modules" - ] + "installed_by": ["modules"] }, "picard/collecthsmetrics": { "branch": "master", "git_sha": "49f4e50534fe4b64101e62ea41d5dc43b1324358", - "installed_by": [ - "modules" - ], + "installed_by": ["modules"], "patch": "modules/nf-core/picard/collecthsmetrics/picard-collecthsmetrics.diff" }, "picard/collectmultiplemetrics": { "branch": "master", "git_sha": "49f4e50534fe4b64101e62ea41d5dc43b1324358", - "installed_by": [ - "modules" - ], + "installed_by": ["modules"], "patch": "modules/nf-core/picard/collectmultiplemetrics/picard-collectmultiplemetrics.diff" }, "picard/collectwgsmetrics": { "branch": "master", "git_sha": "49f4e50534fe4b64101e62ea41d5dc43b1324358", - "installed_by": [ - "modules" - ], + "installed_by": ["modules"], "patch": "modules/nf-core/picard/collectwgsmetrics/picard-collectwgsmetrics.diff" }, "samtools/cat": { "branch": "master", "git_sha": "b13f07be4c508d6ff6312d354d09f2493243e208", - "installed_by": [ - "modules" - ], + "installed_by": ["modules"], "patch": "modules/nf-core/samtools/cat/samtools-cat.diff" }, "samtools/convert": { "branch": "master", "git_sha": "b13f07be4c508d6ff6312d354d09f2493243e208", - "installed_by": [ - "modules" - ], + "installed_by": ["modules"], "patch": "modules/nf-core/samtools/convert/samtools-convert.diff" }, "samtools/coverage": { "branch": "master", "git_sha": "2d20463181b1c38981a02e90d3084b5f9fa8d540", - "installed_by": [ - "modules" - ], + "installed_by": ["modules"], "patch": "modules/nf-core/samtools/coverage/samtools-coverage.diff" }, "samtools/fastq": { "branch": "master", "git_sha": "41dfa3f7c0ffabb96a6a813fe321c6d1cc5b6e46", - "installed_by": [ - "modules" - ] + "installed_by": ["modules"] }, "samtools/flagstat": { "branch": "master", "git_sha": "2d20463181b1c38981a02e90d3084b5f9fa8d540", - "installed_by": [ - "modules" - ] + "installed_by": ["modules"] }, "samtools/idxstats": { "branch": "master", "git_sha": "2d20463181b1c38981a02e90d3084b5f9fa8d540", - "installed_by": [ - "modules" - ] + "installed_by": ["modules"] }, "samtools/import": { "branch": "master", "git_sha": "2d20463181b1c38981a02e90d3084b5f9fa8d540", - "installed_by": [ - "modules" - ] + "installed_by": ["modules"] }, "samtools/index": { "branch": "master", "git_sha": "41dfa3f7c0ffabb96a6a813fe321c6d1cc5b6e46", - "installed_by": [ - "modules" - ] + "installed_by": ["modules"] }, "samtools/sormadup": { "branch": "master", "git_sha": "38f3b0200093498b70ac2d63a83eac5642e3c873", - "installed_by": [ - "modules" - ], + "installed_by": ["modules"], "patch": "modules/nf-core/samtools/sormadup/samtools-sormadup.diff" }, "samtools/sort": { "branch": "master", "git_sha": "b7800db9b069ed505db3f9d91b8c72faea9be17b", - "installed_by": [ - "modules" - ], + "installed_by": ["modules"], "patch": "modules/nf-core/samtools/sort/samtools-sort.diff" }, "samtools/stats": { "branch": "master", "git_sha": "2d20463181b1c38981a02e90d3084b5f9fa8d540", - "installed_by": [ - "modules" - ], + "installed_by": ["modules"], "patch": "modules/nf-core/samtools/stats/samtools-stats.diff" }, "snapaligner/align": { "branch": "master", "git_sha": "77bdd7e1047d2abe21ae8d89acc295ea553ecbae", - "installed_by": [ - "fastq_align_dna", - "modules" - ], + "installed_by": ["fastq_align_dna", "modules"], "patch": "modules/nf-core/snapaligner/align/snapaligner-align.diff" }, "star/align": { "branch": "master", "git_sha": "3c259f0ac1ed9ae3f6b835461d054edffc60120e", - "installed_by": [ - "modules" - ], + "installed_by": ["modules"], "patch": "modules/nf-core/star/align/star-align.diff" } } @@ -289,40 +214,30 @@ "bcl_demultiplex": { "branch": "master", "git_sha": "27cceb2eb8aa959d4a8819caab886386a59a3789", - "installed_by": [ - "subworkflows" - ] + "installed_by": ["subworkflows"] }, "fastq_align_dna": { "branch": "master", "git_sha": "a29f18660f5e3748d44d6f716241e70c942c065d", - "installed_by": [ - "subworkflows" - ] + "installed_by": ["subworkflows"] }, "utils_nextflow_pipeline": { "branch": "master", "git_sha": "56372688d8979092cafbe0c5c3895b491166ca1c", - "installed_by": [ - "subworkflows" - ] + "installed_by": ["subworkflows"] }, "utils_nfcore_pipeline": { "branch": "master", "git_sha": "1b6b9a3338d011367137808b49b923515080e3ba", - "installed_by": [ - "subworkflows" - ] + "installed_by": ["subworkflows"] }, "utils_nfschema_plugin": { "branch": "master", "git_sha": "2fd2cd6d0e7b273747f32e465fdc6bcc3ae0814e", - "installed_by": [ - "subworkflows" - ] + "installed_by": ["subworkflows"] } } } } } -} \ No newline at end of file +} diff --git a/modules/nf-core/fgbio/copyumifromreadname/fgbio-copyumifromreadname.diff b/modules/nf-core/fgbio/copyumifromreadname/fgbio-copyumifromreadname.diff deleted file mode 100644 index 7a934f9f..00000000 --- a/modules/nf-core/fgbio/copyumifromreadname/fgbio-copyumifromreadname.diff +++ /dev/null @@ -1,22 +0,0 @@ -Changes in component 'nf-core/fgbio/copyumifromreadname' -'modules/nf-core/fgbio/copyumifromreadname/environment.yml' is unchanged -Changes in 'fgbio/copyumifromreadname/main.nf': ---- modules/nf-core/fgbio/copyumifromreadname/main.nf -+++ modules/nf-core/fgbio/copyumifromreadname/main.nf -@@ -11,8 +11,8 @@ - tuple val(meta), path(bam), path(bai) - - output: -- tuple val(meta), path("*.bam"), emit: bam -- tuple val(meta), path("*.bai"), emit: bai -+ tuple val(meta), path("${meta.samplename}_ubam.bam"), emit: bam -+ tuple val(meta), path("${meta.samplename}_ubai.bai"), emit: bai - path "versions.yml" , emit: versions - - when: - -'modules/nf-core/fgbio/copyumifromreadname/meta.yml' is unchanged -'modules/nf-core/fgbio/copyumifromreadname/tests/main.nf.test' is unchanged -'modules/nf-core/fgbio/copyumifromreadname/tests/main.nf.test.snap' is unchanged -'modules/nf-core/fgbio/copyumifromreadname/tests/nextflow.config' is unchanged -************************************************************ diff --git a/modules/nf-core/fgbio/copyumifromreadname/main.nf b/modules/nf-core/fgbio/copyumifromreadname/main.nf index 62f04904..b15c970a 100644 --- a/modules/nf-core/fgbio/copyumifromreadname/main.nf +++ b/modules/nf-core/fgbio/copyumifromreadname/main.nf @@ -11,8 +11,8 @@ process FGBIO_COPYUMIFROMREADNAME { tuple val(meta), path(bam), path(bai) output: - tuple val(meta), path("${meta.samplename}_ubam.bam"), emit: bam - tuple val(meta), path("${meta.samplename}_ubai.bai"), emit: bai + tuple val(meta), path("*.bam"), emit: bam + tuple val(meta), path("*.bai"), emit: bai path "versions.yml" , emit: versions when: diff --git a/modules/nf-core/fgbio/fastqtobam/fgbio-fastqtobam.diff b/modules/nf-core/fgbio/fastqtobam/fgbio-fastqtobam.diff deleted file mode 100644 index bac2fe9a..00000000 --- a/modules/nf-core/fgbio/fastqtobam/fgbio-fastqtobam.diff +++ /dev/null @@ -1,25 +0,0 @@ -Changes in component 'nf-core/fgbio/fastqtobam' -'modules/nf-core/fgbio/fastqtobam/environment.yml' is unchanged -Changes in 'fgbio/fastqtobam/main.nf': ---- modules/nf-core/fgbio/fastqtobam/main.nf -+++ modules/nf-core/fgbio/fastqtobam/main.nf -@@ -11,8 +11,8 @@ - tuple val(meta), path(reads) - - output: -- tuple val(meta), path("*.bam") , emit: bam , optional: true -- tuple val(meta), path("*.cram"), emit: cram, optional: true -+ tuple val(meta), path("${meta.samplename}_ubam.bam") , emit: bam , optional: true -+ tuple val(meta), path("${meta.samplename}_ucram.cram"), emit: cram, optional: true - path "versions.yml" , emit: versions - - when: - -'modules/nf-core/fgbio/fastqtobam/meta.yml' is unchanged -'modules/nf-core/fgbio/fastqtobam/tests/bam.config' is unchanged -'modules/nf-core/fgbio/fastqtobam/tests/cram.config' is unchanged -'modules/nf-core/fgbio/fastqtobam/tests/custom_sample.config' is unchanged -'modules/nf-core/fgbio/fastqtobam/tests/main.nf.test' is unchanged -'modules/nf-core/fgbio/fastqtobam/tests/main.nf.test.snap' is unchanged -'modules/nf-core/fgbio/fastqtobam/tests/umi.config' is unchanged -************************************************************ diff --git a/modules/nf-core/fgbio/fastqtobam/main.nf b/modules/nf-core/fgbio/fastqtobam/main.nf index ff8dba76..6ee64bb3 100644 --- a/modules/nf-core/fgbio/fastqtobam/main.nf +++ b/modules/nf-core/fgbio/fastqtobam/main.nf @@ -11,8 +11,8 @@ process FGBIO_FASTQTOBAM { tuple val(meta), path(reads) output: - tuple val(meta), path("${meta.samplename}_ubam.bam") , emit: bam , optional: true - tuple val(meta), path("${meta.samplename}_ucram.cram"), emit: cram, optional: true + tuple val(meta), path("*.bam") , emit: bam , optional: true + tuple val(meta), path("*.cram"), emit: cram, optional: true path "versions.yml" , emit: versions when: diff --git a/nextflow.config b/nextflow.config index cabaec67..4ed16b08 100644 --- a/nextflow.config +++ b/nextflow.config @@ -291,3 +291,15 @@ validation { // Load modules.config for DSL2 module specific options includeConfig 'conf/modules.config' + + +// CONSENSUS SUBWORKFLOW - MODULE EXT SETTINGS + +process { + withName: 'FGBIO_FASTQTOBAM' { + ext.prefix = { "${meta.id}_ubam" } + } + withName: 'FGBIO_COPYUMIFROMREADNAME' { + ext.prefix = { "${meta.id}_copyumifromreadname" } + } +} diff --git a/tests/subworkflows/local/consensus/main.nf.test.snap b/tests/subworkflows/local/consensus/main.nf.test.snap index 671d9784..45845b6e 100644 --- a/tests/subworkflows/local/consensus/main.nf.test.snap +++ b/tests/subworkflows/local/consensus/main.nf.test.snap @@ -2,25 +2,35 @@ "test_consensus_reads_md5": { "content": [ [ - + "c21b5ba7ecc759f9d729a98420921e1e" ], [ - + "d41d8cd98f00b204e9800998ecf8427e" ], [ - + "d41d8cd98f00b204e9800998ecf8427e" ], [ - + "d41d8cd98f00b204e9800998ecf8427e" ], [ - "versions.yml:md5,92ec3e7cd231a6503321464eb83e6955" + "versions.yml:md5,37ec8cc6d3cdb55f06e84f325f511538", + "versions.yml:md5,383381d42173a31f86cce3ab6cf7299e", + "versions.yml:md5,5d6ccca9089e3268bb387820e8ba8f57", + "versions.yml:md5,5fa2a734f2d547c5de3b14c3a2dc5c02", + "versions.yml:md5,7e52768cb8257260977e38d745e51237", + "versions.yml:md5,92ec3e7cd231a6503321464eb83e6955", + "versions.yml:md5,a21193341a7ae0f02ad5ce8af54c1d4a", + "versions.yml:md5,ae3cd5c66636afc64cedd4e633531f2a", + "versions.yml:md5,b081ec66fb0e82580f58170743e4b910", + "versions.yml:md5,d4e922f9ba8e8b393a2994eed8fd89ae", + "versions.yml:md5,faa4838294afcfe9d81dc85e268903e5" ] ], "meta": { "nf-test": "0.9.2", "nextflow": "25.04.6" }, - "timestamp": "2025-08-25T11:10:48.133975221" + "timestamp": "2025-08-25T20:27:23.196481598" } } \ No newline at end of file From b5f32ad71e2cd3cc21dd6d9d2b65fd75df21bd01 Mon Sep 17 00:00:00 2001 From: hwjeong Date: Mon, 25 Aug 2025 20:57:01 +0200 Subject: [PATCH 28/33] nf-test.config: added params --- tests/config/nf-test.config | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/config/nf-test.config b/tests/config/nf-test.config index 54b7070e..c429ebe1 100644 --- a/tests/config/nf-test.config +++ b/tests/config/nf-test.config @@ -15,6 +15,15 @@ aws { } } -plugins = [ - "nft-bam" -] +params { + genomes { + GRCh38 { + bwamem = "s3://test-data/genomics/homo_sapiens/genome/bwa/" + dict = "s3://test-data/genomics/homo_sapiens/genome/seq/GCA_000001405.15_GRCh38_full_plus_hs38d1_analysis_set_chr21.dict" + fai = "s3://test-data/genomics/homo_sapiens/genome/seq/GCA_000001405.15_GRCh38_full_plus_hs38d1_analysis_set_chr21.fna.fai" + fasta = "s3://test-data/genomics/homo_sapiens/genome/seq/GCA_000001405.15_GRCh38_full_plus_hs38d1_analysis_set_chr21.fna" + star = "s3://test-data/genomics/homo_sapiens/genome/star/" + gtf = "s3://test-data/genomics/homo_sapiens/genome/seq/GCA_000001405.15_GRCh38_full_plus_hs38d1_analysis_set_chr21.gtf" + } + } +} From ba11f4982358da78c9cbfa76a282d912ded1f2b3 Mon Sep 17 00:00:00 2001 From: hwjeong Date: Tue, 26 Aug 2025 10:24:28 +0200 Subject: [PATCH 29/33] consensus_bam -> cram (for further integration into pipeline) --- subworkflows/local/consensus/main.nf | 59 +++++++++++++------ .../subworkflows/local/consensus/main.nf.test | 4 +- .../local/consensus/main.nf.test.snap | 9 ++- 3 files changed, 50 insertions(+), 22 deletions(-) diff --git a/subworkflows/local/consensus/main.nf b/subworkflows/local/consensus/main.nf index c3b11f6d..0c3a317a 100644 --- a/subworkflows/local/consensus/main.nf +++ b/subworkflows/local/consensus/main.nf @@ -1,23 +1,25 @@ #!/usr/bin/env nextflow -include { FGBIO_COPYUMIFROMREADNAME } from '../../../modules/nf-core/fgbio/copyumifromreadname/main' -include { FGBIO_CALLMOLECULARCONSENSUSREADS } from '../../../modules/nf-core/fgbio/callmolecularconsensusreads/main' -include { FGBIO_FASTQTOBAM as FASTQTOBAM_READNAME } from '../../../modules/nf-core/fgbio/fastqtobam/main' -include { FGBIO_FASTQTOBAM as FASTQTOBAM_SEQ } from '../../../modules/nf-core/fgbio/fastqtobam/main' -include { FGBIO_FILTERCONSENSUSREADS } from '../../../modules/nf-core/fgbio/filterconsensusreads/main' -include { FGBIO_GROUPREADSBYUMI } from '../../../modules/nf-core/fgbio/groupreadsbyumi/main' -include { FGBIO_SORTBAM } from '../../../modules/nf-core/fgbio/sortbam/main' -include { FGBIO_ZIPPERBAMS as FGBIO_ZIPPERBAMS_RAW } from '../../../modules/nf-core/fgbio/zipperbams/main' -include { FGBIO_ZIPPERBAMS as FGBIO_ZIPPERBAMS_CONS } from '../../../modules/nf-core/fgbio/zipperbams/main' -include { SAMTOOLS_FASTQ as SAMTOOLS_FASTQ_RAW } from '../../../modules/nf-core/samtools/fastq/main' -include { SAMTOOLS_FASTQ as SAMTOOLS_FASTQ_CONS } from '../../../modules/nf-core/samtools/fastq/main' -include { SAMTOOLS_INDEX } from '../../../modules/nf-core/samtools/index/main' -include { FASTQ_ALIGN_DNA as FASTQ_ALIGN_DNA_RAW } from '../../../subworkflows/nf-core/fastq_align_dna/main' -include { FASTQ_ALIGN_DNA as FASTQ_ALIGN_DNA_CONS } from '../../../subworkflows/nf-core/fastq_align_dna/main' +include { FGBIO_COPYUMIFROMREADNAME } from '../../../modules/nf-core/fgbio/copyumifromreadname/main' +include { FGBIO_CALLMOLECULARCONSENSUSREADS } from '../../../modules/nf-core/fgbio/callmolecularconsensusreads/main' +include { FGBIO_FASTQTOBAM as FASTQTOBAM_READNAME } from '../../../modules/nf-core/fgbio/fastqtobam/main' +include { FGBIO_FASTQTOBAM as FASTQTOBAM_SEQ } from '../../../modules/nf-core/fgbio/fastqtobam/main' +include { FGBIO_FILTERCONSENSUSREADS } from '../../../modules/nf-core/fgbio/filterconsensusreads/main' +include { FGBIO_GROUPREADSBYUMI } from '../../../modules/nf-core/fgbio/groupreadsbyumi/main' +include { FGBIO_SORTBAM } from '../../../modules/nf-core/fgbio/sortbam/main' +include { FGBIO_ZIPPERBAMS as FGBIO_ZIPPERBAMS_RAW } from '../../../modules/nf-core/fgbio/zipperbams/main' +include { FGBIO_ZIPPERBAMS as FGBIO_ZIPPERBAMS_CONS } from '../../../modules/nf-core/fgbio/zipperbams/main' +include { SAMTOOLS_FASTQ as SAMTOOLS_FASTQ_RAW } from '../../../modules/nf-core/samtools/fastq/main' +include { SAMTOOLS_FASTQ as SAMTOOLS_FASTQ_CONS } from '../../../modules/nf-core/samtools/fastq/main' +include { SAMTOOLS_INDEX as SAMTOOLS_INDEX_UMI } from '../../../modules/nf-core/samtools/index/main' +include { SAMTOOLS_INDEX as SAMTOOLS_INDEX_CONSENSUS } from '../../../modules/nf-core/samtools/index/main' +include { SAMTOOLS_CONVERT as SAMTOOLS_CONVERT } from '../../../modules/nf-core/samtools/convert/main' +include { FASTQ_ALIGN_DNA as FASTQ_ALIGN_DNA_RAW } from '../../../subworkflows/nf-core/fastq_align_dna/main' +include { FASTQ_ALIGN_DNA as FASTQ_ALIGN_DNA_CONS } from '../../../subworkflows/nf-core/fastq_align_dna/main' workflow CONSENSUS { take: - ch_umi_fastq // channel: [meta_with_readgroup, fastq] for SE/PE/duplex samples + ch_umi_fastq // channel: [meta, fastq1, fastq2] main: def ch_versions = Channel.empty() @@ -37,11 +39,11 @@ workflow CONSENSUS { FASTQTOBAM_READNAME(ch_fastq.readname) ch_versions = ch_versions.mix(FASTQTOBAM_READNAME.out.versions) - SAMTOOLS_INDEX(FASTQTOBAM_READNAME.out.bam) - ch_versions = ch_versions.mix(SAMTOOLS_INDEX.out.versions) + SAMTOOLS_INDEX_UMI(FASTQTOBAM_READNAME.out.bam) + ch_versions = ch_versions.mix(SAMTOOLS_INDEX_UMI.out.versions) FASTQTOBAM_READNAME.out.bam - .join(SAMTOOLS_INDEX.out.bai, by: 0) + .join(SAMTOOLS_INDEX_UMI.out.bai, by: 0) .map { meta, bam, bai -> tuple(meta, bam, bai) } .set { ch_ubam_with_bai } @@ -96,7 +98,6 @@ workflow CONSENSUS { ch_versions = ch_versions.mix(FGBIO_ZIPPERBAMS_RAW.out.versions) - // 1.3: Mapped BAM => Grouped BAM @@ -180,11 +181,31 @@ workflow CONSENSUS { def ch_consensus_filtered_bam = FGBIO_SORTBAM.out.bam + // Consensus_filtered_bam into CRAM (integration into pipeline) + + SAMTOOLS_INDEX_CONSENSUS(ch_consensus_filtered_bam) + ch_versions = ch_versions.mix(SAMTOOLS_INDEX_CONSENSUS.out.versions) + + def ch_sam_convert_bai_fasta_fai = SAMTOOLS_INDEX_CONSENSUS.out.bai.map {meta, bai -> + def gd = (meta.genome_data instanceof Map) ? meta.genome_data : [:] + def fasta = file(gd.fasta, checkIfExists: true) + def fai = file(gd.fai, checkIfExists: true) + tuple(meta, bai, fasta, fai) + } + + def ch_consensus_bam_convert = ch_consensus_filtered_bam + .join(ch_sam_convert_bai_fasta_fai, by: 0) + + SAMTOOLS_CONVERT(ch_consensus_bam_convert) + + ch_consensus_cram = SAMTOOLS_CONVERT.out.cram + ch_versions = ch_versions.mix(SAMTOOLS_CONVERT.out.versions) emit: ubam = ch_ubam consensus_bam = ch_consensus_filtered_bam grouped_bam = ch_grouped_bam filtered_ubam = ch_filtered_uBam + consensus_cram = ch_consensus_cram versions = ch_versions } diff --git a/tests/subworkflows/local/consensus/main.nf.test b/tests/subworkflows/local/consensus/main.nf.test index 1a759193..5fce76ef 100644 --- a/tests/subworkflows/local/consensus/main.nf.test +++ b/tests/subworkflows/local/consensus/main.nf.test @@ -26,7 +26,8 @@ nextflow_workflow { genome_data: [ fasta : params.genomes.GRCh38.fasta, dict : params.genomes.GRCh38.dict, - bwamem: params.genomes.GRCh38.bwamem + bwamem: params.genomes.GRCh38.bwamem, + fai : params.genomes.GRCh38.fai ] ], file('https://raw.githubusercontent.com/nf-core/test-datasets/modules/data/genomics/homo_sapiens/illumina/fastq/test.umi_1.fastq.gz'), @@ -44,6 +45,7 @@ nextflow_workflow { workflow.out.grouped_bam.collect { bam(it[1]).getReadsMD5() }, workflow.out.filtered_ubam.collect { bam(it[1]).getReadsMD5() }, workflow.out.consensus_bam.collect { bam(it[1]).getReadsMD5() }, + workflow.out.consensus_cram.collect { bam(it[1]).getReadsMD5() }, workflow.out.versions ).match() } diff --git a/tests/subworkflows/local/consensus/main.nf.test.snap b/tests/subworkflows/local/consensus/main.nf.test.snap index 45845b6e..f19396bf 100644 --- a/tests/subworkflows/local/consensus/main.nf.test.snap +++ b/tests/subworkflows/local/consensus/main.nf.test.snap @@ -13,9 +13,13 @@ [ "d41d8cd98f00b204e9800998ecf8427e" ], + [ + "d41d8cd98f00b204e9800998ecf8427e" + ], [ "versions.yml:md5,37ec8cc6d3cdb55f06e84f325f511538", "versions.yml:md5,383381d42173a31f86cce3ab6cf7299e", + "versions.yml:md5,4af2c1a5032fc1ce5ec09368f956480c", "versions.yml:md5,5d6ccca9089e3268bb387820e8ba8f57", "versions.yml:md5,5fa2a734f2d547c5de3b14c3a2dc5c02", "versions.yml:md5,7e52768cb8257260977e38d745e51237", @@ -24,13 +28,14 @@ "versions.yml:md5,ae3cd5c66636afc64cedd4e633531f2a", "versions.yml:md5,b081ec66fb0e82580f58170743e4b910", "versions.yml:md5,d4e922f9ba8e8b393a2994eed8fd89ae", - "versions.yml:md5,faa4838294afcfe9d81dc85e268903e5" + "versions.yml:md5,faa4838294afcfe9d81dc85e268903e5", + "versions.yml:md5,faf05dada3834ff602901efc1a334351" ] ], "meta": { "nf-test": "0.9.2", "nextflow": "25.04.6" }, - "timestamp": "2025-08-25T20:27:23.196481598" + "timestamp": "2025-08-26T10:22:28.128610654" } } \ No newline at end of file From e4394e0671f0bd6f62656efda978e022f419312c Mon Sep 17 00:00:00 2001 From: hwjeong Date: Tue, 26 Aug 2025 11:17:54 +0200 Subject: [PATCH 30/33] UMI samples integrated with non-UMI samples --- subworkflows/local/consensus/main.nf | 9 ++++++--- tests/subworkflows/local/consensus/main.nf.test | 2 +- tests/subworkflows/local/consensus/main.nf.test.snap | 2 +- workflows/preprocessing.nf | 8 +++----- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/subworkflows/local/consensus/main.nf b/subworkflows/local/consensus/main.nf index 0c3a317a..5a3e6189 100644 --- a/subworkflows/local/consensus/main.nf +++ b/subworkflows/local/consensus/main.nf @@ -197,15 +197,18 @@ workflow CONSENSUS { .join(ch_sam_convert_bai_fasta_fai, by: 0) SAMTOOLS_CONVERT(ch_consensus_bam_convert) - - ch_consensus_cram = SAMTOOLS_CONVERT.out.cram ch_versions = ch_versions.mix(SAMTOOLS_CONVERT.out.versions) + SAMTOOLS_CONVERT.out.cram + .join(SAMTOOLS_CONVERT.out.crai, by: 0) + .map { meta, cram, crai -> tuple(meta, cram, crai) } + .set { ch_consensus_cram_crai } + emit: ubam = ch_ubam consensus_bam = ch_consensus_filtered_bam grouped_bam = ch_grouped_bam filtered_ubam = ch_filtered_uBam - consensus_cram = ch_consensus_cram + consensus_cram_crai = ch_consensus_cram_crai versions = ch_versions } diff --git a/tests/subworkflows/local/consensus/main.nf.test b/tests/subworkflows/local/consensus/main.nf.test index 5fce76ef..67be60b7 100644 --- a/tests/subworkflows/local/consensus/main.nf.test +++ b/tests/subworkflows/local/consensus/main.nf.test @@ -45,7 +45,7 @@ nextflow_workflow { workflow.out.grouped_bam.collect { bam(it[1]).getReadsMD5() }, workflow.out.filtered_ubam.collect { bam(it[1]).getReadsMD5() }, workflow.out.consensus_bam.collect { bam(it[1]).getReadsMD5() }, - workflow.out.consensus_cram.collect { bam(it[1]).getReadsMD5() }, + workflow.out.consensus_cram_crai.collect { bam(it[1]).getReadsMD5() }, workflow.out.versions ).match() } diff --git a/tests/subworkflows/local/consensus/main.nf.test.snap b/tests/subworkflows/local/consensus/main.nf.test.snap index f19396bf..166b52a1 100644 --- a/tests/subworkflows/local/consensus/main.nf.test.snap +++ b/tests/subworkflows/local/consensus/main.nf.test.snap @@ -36,6 +36,6 @@ "nf-test": "0.9.2", "nextflow": "25.04.6" }, - "timestamp": "2025-08-26T10:22:28.128610654" + "timestamp": "2025-08-26T11:16:54.790224281" } } \ No newline at end of file diff --git a/workflows/preprocessing.nf b/workflows/preprocessing.nf index ec95ab63..bb054bb9 100644 --- a/workflows/preprocessing.nf +++ b/workflows/preprocessing.nf @@ -197,7 +197,7 @@ workflow PREPROCESSING { /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -// STEP: UMI CONSENSUS +// STEP: UMI CONSENSUS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ @@ -212,10 +212,7 @@ workflow PREPROCESSING { CONSENSUS(ch_umi_fastq) ch_versions = ch_versions.mix(CONSENSUS.out.versions) - def ch_ubam_for_umi = CONSENSUS.out.ubam - def ch_umi_consensus_bam = CONSENSUS.out.consensus_bam - def ch_umi_grouped_bam = CONSENSUS.out.grouped_bam - + def ch_consensus_cram_crai = CONSENSUS.out.consensus_cram_crai /* @@ -306,6 +303,7 @@ workflow PREPROCESSING { */ FASTQ_TO_CRAM.out.cram_crai + .mix(ch_consensus_cram_crai) .filter{ meta, cram, crai -> meta.tag != "SNP" } From b8bdafa517d5844027fbb6b6c0f6f96d09675728 Mon Sep 17 00:00:00 2001 From: Dhamar Date: Tue, 26 Aug 2025 14:25:02 +0200 Subject: [PATCH 31/33] Update on documentation --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 0964e70c..5e0e67ce 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ It also performs basic QC and coverage analysis. The pipeline is built using Nextflow, a workflow tool to run tasks across multiple compute infrastructures in a very portable manner. It comes with docker containers making installation trivial and results highly reproducible. +The pipeline also supports Unique Molecular Identifier (UMI) data. If your samplesheet includes a `umi_type` column (`seq` or `readname`), UMI-aware preprocessing is enabled automatically; rows with `umi_type=none` are processed as usual. + Steps inlcude: 1. Demultiplexing using [`BCLconvert`](https://emea.support.illumina.com/sequencing/sequencing_software/bcl-convert.html) @@ -26,6 +28,12 @@ Steps inlcude: ![metro map](docs/images/metro_map.png) +UMI processing (only for rows with `umi_type`): +- Extract UMI from read sequence (`seq`) or read name (`readname`) +- Group reads by UMI (fgbio GroupReadsByUmi) +- Call molecular consensus (fgbio CallMolecularConsensusReads) and filter (fgbio FilterConsensusReads) +- Re-align filtered consensus reads with BWA-MEM (`-Y`), then sort/index + ## Usage > [!NOTE] @@ -41,6 +49,12 @@ First, prepare a samplesheet with your input data that looks as follows: id,samplename,organism,library,fastq_1,fastq_2 sample1,sample1,Homo sapiens,Library_Name,reads1.fq.gz,reads2.fq.gz ``` +`samplesheet.csv` for fastq inputs with UMI: + +```csv +id,samplename,organism,library,umi_type,fastq_1,fastq_2 +umi_sample1,umi_sample1,Homo sapiens,Library_Name,seq,reads1.fq.gz,reads2.fq.gz +``` `samplesheet.csv` for flowcell inputs: From d78154f7c95c8a09aaa66ef83705b8f2c2cad12b Mon Sep 17 00:00:00 2001 From: hwjeong Date: Wed, 27 Aug 2025 10:27:59 +0200 Subject: [PATCH 32/33] Potential parameters added as comments (feedback by Toon) --- nextflow.config | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/nextflow.config b/nextflow.config index 4ed16b08..2108dad4 100644 --- a/nextflow.config +++ b/nextflow.config @@ -34,6 +34,12 @@ params { filterconsensusreads_min_baseq = 45 filterconsensusreads_min_base_error_rate = 0.2 + // UMI options according to KAPA HyperPlex UMI kit (potentially used in the future) + // callmolecularconsensusreads_max_reads = 50 + // callmolecularconsensusreads_output_per_base_tags = false + // callmolecularconsensusreads_read_name_prefix = 'consensus' + + // References genomes = [:] igenomes_base = '/references' From 12d2e2388b646d57a4bcc2fb91173f6a33b4352b Mon Sep 17 00:00:00 2001 From: Dhamar Date: Wed, 27 Aug 2025 16:20:25 +0200 Subject: [PATCH 33/33] Documentation corection and metro map update --- README.md | 4 +- docs/images/metro_mapumi.png | Bin 0 -> 231950 bytes docs/images/metro_mapumi.svg | 1351 ++++++++++++++++++++++++++++++++++ 3 files changed, 1353 insertions(+), 2 deletions(-) create mode 100644 docs/images/metro_mapumi.png create mode 100644 docs/images/metro_mapumi.svg diff --git a/README.md b/README.md index 5e0e67ce..ddffa516 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ It also performs basic QC and coverage analysis. The pipeline is built using Nextflow, a workflow tool to run tasks across multiple compute infrastructures in a very portable manner. It comes with docker containers making installation trivial and results highly reproducible. -The pipeline also supports Unique Molecular Identifier (UMI) data. If your samplesheet includes a `umi_type` column (`seq` or `readname`), UMI-aware preprocessing is enabled automatically; rows with `umi_type=none` are processed as usual. +The pipeline also supports Unique Molecular Identifier (UMI) data. If your samplesheet includes a `umi_type` column (`seq` or `readname`), UMI-aware preprocessing is enabled automatically. Rows with no `umi_type` specified will be processed as non-UMI sequencing data. Steps inlcude: @@ -26,7 +26,7 @@ Steps inlcude: 6. Alignment QC using [`samtools flagstat`](http://www.htslib.org/doc/samtools-flagstat.html), [`samtools stats`](http://www.htslib.org/doc/samtools-stats.html), [`samtools idxstats`](http://www.htslib.org/doc/samtools-idxstats.html) and [`picard CollecHsMetrics`](https://broadinstitute.github.io/picard/command-line-overview.html#CollectHsMetrics), [`picard CollectWgsMetrics`](https://broadinstitute.github.io/picard/command-line-overview.html#CollectWgsMetrics), [`picard CollectMultipleMetrics`](https://broadinstitute.github.io/picard/command-line-overview.html#CollectMultipleMetrics) 7. QC aggregation using [`multiqc`](https://multiqc.info/) -![metro map](docs/images/metro_map.png) +![metro map](docs/images/metro_mapumi.png) UMI processing (only for rows with `umi_type`): - Extract UMI from read sequence (`seq`) or read name (`readname`) diff --git a/docs/images/metro_mapumi.png b/docs/images/metro_mapumi.png new file mode 100644 index 0000000000000000000000000000000000000000..56b43b759489589dbbb41a4cc10fdb5056eb47f3 GIT binary patch literal 231950 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 z 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

AhK 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

!7yCIQOmAG#QZ||^r3y268G(6+?spVm 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 D1> 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 BiK+kq literal 0 HcmV?d00001 diff --git a/docs/images/metro_mapumi.svg b/docs/images/metro_mapumi.svg new file mode 100644 index 00000000..73f54ac2 --- /dev/null +++ b/docs/images/metro_mapumi.svg @@ -0,0 +1,1351 @@ + + + +nf-cmgg/preprocessingfastqflowcellfastqORfastqfastqbclbclbclbclbclFastP trimming,QC andadapterremovalAlign +bowtie2, bwamem, +bwamem2, snap +dragmap or starUMI preprocessing +copy UMI + group +Consensus post-align +zipperbams + sort +Consensus reads +call + filter +cramMultiQCreportindexfastaORMandatory process(es)UMI flowSupported genomes flowReport filesLegendsOptional process(es)Additional outputs/inputsUnsupported genomes flowCreate indexwhen index ismissing Sort & mark duplicatessamtools sormadup or bamsormadupsamtools convertbam -> crambedmosdepthsamtools import +fastq -> ucramPicard metricspicard CollectMultipleMetrics,picard CollectWgsMetrics andpicard CollectHsMetrics--disable_picard_metrics falsesamtools metricssamtools stats,samtools flagstat andsamtools idxstatsucramreportsAll generatedreportssamtools cat +merge ucrams +from same samplebcl-convertbcl -> fastqinteropdetermine coveragein genelist panelssamtoolscoveragebed