diff --git a/subworkflows/nf-core/ubam_align_minimap2/main.nf b/subworkflows/nf-core/ubam_align_minimap2/main.nf new file mode 100644 index 000000000000..fcd4afea103e --- /dev/null +++ b/subworkflows/nf-core/ubam_align_minimap2/main.nf @@ -0,0 +1,65 @@ +// +// Optionally concatenate uBAM files, align with minimap2, index BAM, and run BAM stats. +// + +include { SAMTOOLS_CAT } from '../../../modules/nf-core/samtools/cat/main' +include { MINIMAP2_ALIGN } from '../../../modules/nf-core/minimap2/align/main' +include { BAM_STATS_SAMTOOLS } from '../bam_stats_samtools/main' + +workflow UBAM_ALIGN_MINIMAP2 { + + take: + ch_ubam + // [ val(meta), path(ubam) ] + // ubam may be one BAM/uBAM file or a list of BAM/uBAM files. + + ch_fasta_fai + // [ val(meta2), path(fasta), path(fai) ] + + val_bam_index_extension + // "bai" or "csi" + + val_cigar_bam + // true/false + + val_skip_cat + // true: pass one uBAM directly to minimap2 + // false: concatenate uBAM files with samtools/cat before minimap2 + + main: + + ch_fasta = ch_fasta_fai.map { meta, fasta, _fai -> [ meta, fasta ] } + + ch_cat_ubam = channel.empty() + + if (val_skip_cat) { + ch_ubam_for_alignment = ch_ubam + } else { + SAMTOOLS_CAT(ch_ubam) + ch_ubam_for_alignment = SAMTOOLS_CAT.out.bam + ch_cat_ubam = SAMTOOLS_CAT.out.bam + } + + MINIMAP2_ALIGN( + ch_ubam_for_alignment, + ch_fasta, + true, + val_bam_index_extension, + false, + val_cigar_bam + ) + + MINIMAP2_ALIGN.out.bam + .join(MINIMAP2_ALIGN.out.index, by: 0, failOnDuplicate: true, failOnMismatch: true) + .set { ch_bam_bai } + + BAM_STATS_SAMTOOLS(ch_bam_bai, ch_fasta_fai) + + emit: + ubam = ch_cat_ubam + bam = MINIMAP2_ALIGN.out.bam + index = MINIMAP2_ALIGN.out.index + stats = BAM_STATS_SAMTOOLS.out.stats + flagstat = BAM_STATS_SAMTOOLS.out.flagstat + idxstats = BAM_STATS_SAMTOOLS.out.idxstats +} diff --git a/subworkflows/nf-core/ubam_align_minimap2/meta.yml b/subworkflows/nf-core/ubam_align_minimap2/meta.yml new file mode 100644 index 000000000000..1b6ab290b336 --- /dev/null +++ b/subworkflows/nf-core/ubam_align_minimap2/meta.yml @@ -0,0 +1,67 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/subworkflows/yaml-schema.json +name: ubam_align_minimap2 +description: Optionally concatenate unaligned BAM files, align reads with minimap2, and run BAM statistics +keywords: + - ubam + - bam + - align + - minimap2 + - long-read +components: + - samtools/cat + - minimap2/align + - bam_stats_samtools + - samtools/stats + - samtools/flagstat + - samtools/idxstats +input: + - ch_ubam: + description: | + Unaligned BAM input channel. + Structure: [ val(meta), path(ubam) ] + ubam may be one BAM/uBAM file or a list of BAM/uBAM files. + Input files should use the .bam extension. + - ch_fasta_fai: + description: | + Reference FASTA and FASTA index. + Structure: [ val(meta2), path(fasta), path(fai) ] + - val_bam_index_extension: + description: | + BAM index extension created by minimap2/align + Typical values are "bai" or "csi". + - val_cigar_bam: + description: | + Whether to use minimap2 long-CIGAR BAM handling + - val_skip_cat: + description: | + If true, pass one uBAM directly to minimap2. + If false, concatenate uBAM files with samtools/cat before alignment +output: + - ubam: + description: | + Concatenated uBAM file, emitted only when val_skip_cat is false + Structure: [ val(meta), path(bam) ] + - bam: + description: | + Coordinate-sorted BAM file from minimap2 + Structure: [ val(meta), path(bam) ] + - index: + description: | + BAM index file. + Structure: [ val(meta), path(index) ] + - stats: + description: | + Samtools stats output from bam_stats_samtools + Structure: [ val(meta), path(stats) ] + - flagstat: + description: | + Samtools flagstat output from bam_stats_samtools + Structure: [ val(meta), path(flagstat) ] + - idxstats: + description: | + Samtools idxstats output from bam_stats_samtools + Structure: [ val(meta), path(idxstats) ] +authors: + - "@manascripts" +maintainers: + - "@manascripts" diff --git a/subworkflows/nf-core/ubam_align_minimap2/tests/main.nf.test b/subworkflows/nf-core/ubam_align_minimap2/tests/main.nf.test new file mode 100644 index 000000000000..fde9819f3f68 --- /dev/null +++ b/subworkflows/nf-core/ubam_align_minimap2/tests/main.nf.test @@ -0,0 +1,154 @@ +nextflow_workflow { + + name "Test Workflow UBAM_ALIGN_MINIMAP2" + script "../main.nf" + workflow "UBAM_ALIGN_MINIMAP2" + config "./nextflow.config" + + tag "subworkflows" + tag "subworkflows_nfcore" + tag "subworkflows/ubam_align_minimap2" + tag "samtools/cat" + tag "samtools/import" + tag "minimap2/align" + tag "subworkflows/bam_stats_samtools" + + setup { + run("SAMTOOLS_IMPORT", alias: "SAMTOOLS_IMPORT_SINGLE") { + script "../../../../modules/nf-core/samtools/import/main.nf" + process { + """ + input[0] = [ + [ id:'test', single_end:true ], + file(params.modules_testdata_base_path + 'genomics/sarscov2/nanopore/fastq/test.fastq.gz', checkIfExists:true) + ] + """ + } + } + + run("SAMTOOLS_IMPORT", alias: "SAMTOOLS_IMPORT_ONE") { + script "../../../../modules/nf-core/samtools/import/main.nf" + process { + """ + input[0] = [ + [ id:'test', single_end:true ], + file(params.modules_testdata_base_path + 'genomics/sarscov2/nanopore/fastq/test.fastq.gz', checkIfExists:true) + ] + """ + } + } + + run("SAMTOOLS_IMPORT", alias: "SAMTOOLS_IMPORT_TWO") { + script "../../../../modules/nf-core/samtools/import/main.nf" + process { + """ + input[0] = [ + [ id:'test', single_end:true ], + file(params.modules_testdata_base_path + 'genomics/sarscov2/nanopore/fastq/test_2.fastq.gz', checkIfExists:true) + ] + """ + } + } + } + + test("nanopore ubam without concatenation") { + when { + workflow { + """ + input[0] = SAMTOOLS_IMPORT_SINGLE.out.bam + + input[1] = Channel.value([ + [ id:'sarscov2' ], + file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta', checkIfExists:true), + file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta.fai', checkIfExists:true) + ]) + + input[2] = 'bai' + input[3] = true + input[4] = true + """ + } + } + + then { + assertAll( + { assert workflow.success }, + { assert file(workflow.out.bam[0][1]).exists() }, + { assert file(workflow.out.index[0][1]).exists() }, + { assert snapshot( + workflow.out.stats, + workflow.out.flagstat, + workflow.out.idxstats + ).match() } + ) + } + } + + test("nanopore ubams with concatenation") { + when { + workflow { + """ + input[0] = SAMTOOLS_IMPORT_ONE.out.bam + .join(SAMTOOLS_IMPORT_TWO.out.bam, by: 0, failOnDuplicate: true, failOnMismatch: true) + .map { meta, ubam_one, ubam_two -> [ meta, [ ubam_one, ubam_two ] ] } + + input[1] = Channel.value([ + [ id:'sarscov2' ], + file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta', checkIfExists:true), + file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta.fai', checkIfExists:true) + ]) + + input[2] = 'bai' + input[3] = true + input[4] = false + """ + } + } + + then { + assertAll( + { assert workflow.success }, + { assert file(workflow.out.ubam[0][1]).exists() }, + { assert file(workflow.out.bam[0][1]).exists() }, + { assert file(workflow.out.index[0][1]).exists() }, + { assert snapshot( + workflow.out.stats, + workflow.out.flagstat, + workflow.out.idxstats + ).match() } + ) + } + } + + test("nanopore ubam without concatenation - stub") { + options "-stub" + + when { + workflow { + """ + input[0] = Channel.of([ + [ id:'test', single_end:true ], + file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/bam/test.single_end.sorted.bam', checkIfExists:true) + ]) + + input[1] = Channel.value([ + [ id:'sarscov2' ], + file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta', checkIfExists:true), + file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta.fai', checkIfExists:true) + ]) + + input[2] = 'bai' + input[3] = true + input[4] = true + """ + } + } + + then { + assertAll( + { assert workflow.success }, + { assert snapshot(workflow.out).match() } + ) + } + } +} diff --git a/subworkflows/nf-core/ubam_align_minimap2/tests/main.nf.test.snap b/subworkflows/nf-core/ubam_align_minimap2/tests/main.nf.test.snap new file mode 100644 index 000000000000..076f32d9d879 --- /dev/null +++ b/subworkflows/nf-core/ubam_align_minimap2/tests/main.nf.test.snap @@ -0,0 +1,181 @@ +{ + "nanopore ubam without concatenation": { + "content": [ + [ + [ + { + "id": "test", + "single_end": true + }, + "test.stats:md5,f965c229fef6f94e405264ca0d6e802f" + ] + ], + [ + [ + { + "id": "test", + "single_end": true + }, + "test.flagstat:md5,ba966c6410eccf2375c3bcc081ef4787" + ] + ], + [ + [ + { + "id": "test", + "single_end": true + }, + "test.idxstats:md5,7c8e67206e2a55b23091c47122a64218" + ] + ] + ], + "timestamp": "2026-07-03T16:29:25.717908188", + "meta": { + "nf-test": "0.9.5", + "nextflow": "25.10.4" + } + }, + "nanopore ubam without concatenation - stub": { + "content": [ + { + "0": [ + + ], + "1": [ + [ + { + "id": "test", + "single_end": true + }, + "test.aligned.bam:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "2": [ + [ + { + "id": "test", + "single_end": true + }, + "test.aligned.bam.bai:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "3": [ + [ + { + "id": "test", + "single_end": true + }, + "test.stats:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "4": [ + [ + { + "id": "test", + "single_end": true + }, + "test.flagstat:md5,67394650dbae96d1a4fcc70484822159" + ] + ], + "5": [ + [ + { + "id": "test", + "single_end": true + }, + "test.idxstats:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "bam": [ + [ + { + "id": "test", + "single_end": true + }, + "test.aligned.bam:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "flagstat": [ + [ + { + "id": "test", + "single_end": true + }, + "test.flagstat:md5,67394650dbae96d1a4fcc70484822159" + ] + ], + "idxstats": [ + [ + { + "id": "test", + "single_end": true + }, + "test.idxstats:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "index": [ + [ + { + "id": "test", + "single_end": true + }, + "test.aligned.bam.bai:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "stats": [ + [ + { + "id": "test", + "single_end": true + }, + "test.stats:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "ubam": [ + + ] + } + ], + "timestamp": "2026-07-03T16:29:46.181944577", + "meta": { + "nf-test": "0.9.5", + "nextflow": "25.10.4" + } + }, + "nanopore ubams with concatenation": { + "content": [ + [ + [ + { + "id": "test", + "single_end": true + }, + "test.stats:md5,906ed17a1dc53cee160625c247f01dba" + ] + ], + [ + [ + { + "id": "test", + "single_end": true + }, + "test.flagstat:md5,dc0c491264218cd55f9778ea833010c8" + ] + ], + [ + [ + { + "id": "test", + "single_end": true + }, + "test.idxstats:md5,3d8ebd6404f1ace85bcf2ee371be1fd7" + ] + ] + ], + "timestamp": "2026-07-03T16:29:37.452934242", + "meta": { + "nf-test": "0.9.5", + "nextflow": "25.10.4" + } + } +} \ No newline at end of file diff --git a/subworkflows/nf-core/ubam_align_minimap2/tests/nextflow.config b/subworkflows/nf-core/ubam_align_minimap2/tests/nextflow.config new file mode 100644 index 000000000000..d572714408a9 --- /dev/null +++ b/subworkflows/nf-core/ubam_align_minimap2/tests/nextflow.config @@ -0,0 +1,9 @@ +// IMPORTANT: Add the ext.prefix configuration to your modules.config +// Avoid input/output filename collisions when minimap2/align receives uBAM input. + +process { + withName: MINIMAP2_ALIGN { + ext.args = '-x map-ont -Y --MD' + ext.prefix = { "${meta.id}.aligned" } + } +}