diff --git a/.github/skip_nf_test.json b/.github/skip_nf_test.json index 91c8e11c16e8..f20e33d0f76a 100644 --- a/.github/skip_nf_test.json +++ b/.github/skip_nf_test.json @@ -46,6 +46,7 @@ "modules/nf-core/deepvariant/vcfstatsreport", "subworkflows/nf-core/dia_proteomics_analysis", "modules/nf-core/diann", + "modules/nf-core/dorado", "modules/nf-core/doubletdetection", "modules/nf-core/fastk/fastk", "modules/nf-core/fastk/histex", diff --git a/.nf-core.yml b/.nf-core.yml index 33226459b958..081e9ed8affb 100644 --- a/.nf-core.yml +++ b/.nf-core.yml @@ -6,6 +6,7 @@ container-registry: - ghcr.io/nbisweden - ghcr.io/scverse - nvcr.io/nvidia/clara + - docker.io/nanoporetech - docker.io/biocontainers - docker.io/labsyspharm - docker.io/google diff --git a/modules/nf-core/dorado/aligner/environment.yml b/modules/nf-core/dorado/aligner/environment.yml new file mode 100644 index 000000000000..6ea28e8c8af8 --- /dev/null +++ b/modules/nf-core/dorado/aligner/environment.yml @@ -0,0 +1,5 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +# dorado is distributed under ONTPL and not on bioconda; use the vendor container. +channels: [] +dependencies: [] diff --git a/modules/nf-core/dorado/aligner/main.nf b/modules/nf-core/dorado/aligner/main.nf new file mode 100644 index 000000000000..0ed46ee6919f --- /dev/null +++ b/modules/nf-core/dorado/aligner/main.nf @@ -0,0 +1,50 @@ +process DORADO_ALIGNER { + tag "$meta.id" + label 'process_high' + + // dorado is not on bioconda (ONTPL licence). Using + // Docker Hub image directly. SHA tag pins to v1.4.0; a semver tag is tracked in + // nanoporetech/dorado#1584. Same pattern as nf-core/parabricks modules. + conda null + container "docker.io/nanoporetech/dorado:shac8f356489fa8b44b31beba841b84d2879de2088e" + + input: + tuple val(meta), path(bam) // unaligned BAM produced by dorado basecaller + tuple val(meta2), path(reference), path(fai) // reference FASTA (or .mmi index) and .fai + + output: + tuple val(meta), path("*.bam") , emit: bam + tuple val(meta), path("*.sequencing_summary.txt"), emit: summary, optional: true + tuple val("${task.process}"), val('dorado'), eval("dorado --version 2>&1 | head -1"), emit: versions_dorado, topic: versions + + when: + task.ext.when == null || task.ext.when + + script: + def args = task.ext.args ?: '' + def prefix = task.ext.prefix ?: "${meta.id}" + """ + dorado \\ + aligner \\ + ${args} \\ + --threads ${task.cpus} \\ + ${reference} \\ + ${bam} \\ + > ${prefix}.bam + + # --emit-summary writes a fixed-name `sequencing_summary.txt` into the work + # directory; prefix it so outputs stay per-sample. + if [ -f sequencing_summary.txt ]; then + mv sequencing_summary.txt ${prefix}.sequencing_summary.txt + fi + """ + + stub: + def args = task.ext.args ?: '' + def prefix = task.ext.prefix ?: "${meta.id}" + def summary = args.contains('--emit-summary') ? "touch ${prefix}.sequencing_summary.txt" : '' + """ + touch ${prefix}.bam + ${summary} + """ +} diff --git a/modules/nf-core/dorado/aligner/meta.yml b/modules/nf-core/dorado/aligner/meta.yml new file mode 100644 index 000000000000..a213613d6c39 --- /dev/null +++ b/modules/nf-core/dorado/aligner/meta.yml @@ -0,0 +1,116 @@ +name: dorado_aligner +description: | + Align unaligned Oxford Nanopore BAM files (typically produced by dorado basecaller) + to a reference genome using the dorado aligner, which wraps minimap2 and + preserves modified base tags (MM/ML) and other BAM auxiliary tags. +keywords: + - alignment + - minimap2 + - ont + - long-read + - methylation + - modified-bases + - nanopore +tools: + - "dorado": + description: Oxford Nanopore's aligner that wraps minimap2 and preserves + modification tags (MM/ML) from basecaller output. + homepage: https://github.com/nanoporetech/dorado + documentation: https://software-docs.nanoporetech.com/dorado/latest/ + tool_dev_url: https://github.com/nanoporetech/dorado + licence: + - "Oxford Nanopore Technologies PLC. Public License Version 1.0" + identifier: "" +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + - bam: + type: file + description: | + BAM file to align, typically the unaligned output of dorado + basecaller. Any HTS-format input is accepted; an already-aligned BAM is + realigned. Modified base tags (MM/ML) and other BAM auxiliary tags are + preserved through alignment. + pattern: "*.bam" + ontologies: + - edam: http://edamontology.org/format_2572 + - - meta2: + type: map + description: | + Groovy Map containing reference genome information + e.g. `[ id:'hg38' ]` + - reference: + type: file + description: | + Reference FASTA (or pre-built minimap2 .mmi index) to align to. + pattern: "*.{fa,fasta,fa.gz,fasta.gz,mmi}" + ontologies: + - edam: http://edamontology.org/format_1929 + - fai: + type: file + description: | + FASTA index (.fai) for the reference. Required when reference is a FASTA. + Pass `[]` when reference is a minimap2 .mmi index. + pattern: "*.fai" + ontologies: + - edam: http://edamontology.org/format_3003 +output: + bam: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + - "*.bam": + type: file + description: | + Aligned BAM with modification tags preserved. Output sort order is + SO:unknown — pipe to SAMTOOLS_SORT + SAMTOOLS_INDEX for coordinate-sorted + indexed BAMs. + pattern: "*.bam" + ontologies: + - edam: http://edamontology.org/format_2572 + summary: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test', single_end:false ]` + - "*.sequencing_summary.txt": + type: file + description: | + Per-read alignment summary, present only when `--emit-summary` is set + in `ext.args`. dorado writes this as a fixed-name + `sequencing_summary.txt`; the module renames it to + `.sequencing_summary.txt` so outputs stay per-sample. + pattern: "*.sequencing_summary.txt" + ontologies: + - edam: http://edamontology.org/format_3475 + versions_dorado: + - - ${task.process}: + type: string + description: The name of the process + - dorado: + type: string + description: The name of the tool + - dorado --version 2>&1 | head -1: + type: eval + description: The expression to obtain the version of the tool +topics: + versions: + - - ${task.process}: + type: string + description: The name of the process + - dorado: + type: string + description: The name of the tool + - dorado --version 2>&1 | head -1: + type: eval + description: The expression to obtain the version of the tool +authors: + - "@sahuno" +maintainers: + - "@sahuno" diff --git a/modules/nf-core/dorado/aligner/tests/main.nf.test b/modules/nf-core/dorado/aligner/tests/main.nf.test new file mode 100644 index 000000000000..e9f86b7353a1 --- /dev/null +++ b/modules/nf-core/dorado/aligner/tests/main.nf.test @@ -0,0 +1,89 @@ +nextflow_process { + + name "Test Process DORADO_ALIGNER" + script "../main.nf" + process "DORADO_ALIGNER" + config "./nextflow.config" + + tag "modules" + tag "modules_nfcore" + tag "dorado" + tag "dorado/aligner" + + test("homo sapiens - nanopore bam - stub") { + + options "-stub" + + when { + process { + """ + input[0] = [ + [ id: 'test' ], + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/nanopore/bam/test2.sorted.bam', checkIfExists: true) + ] + input[1] = [ + [ id: 'genome' ], + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta.fai', checkIfExists: true) + ] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + } + + test("homo sapiens - nanopore bam") { + + // dorado aligner accepts any HTS input; the reads in this fixture are + // realigned against the chr22 subset reference. + // + // Neither a whole-file md5 nor nft-bam's readsMD5 is usable here: the BAM + // header carries environment-dependent values (@PG CL: paths, @SQ UR:), and + // dorado writes records in a thread-dependent order with SO:unknown, so the + // reads md5 differs between runs on the same input. The record count is + // snapshotted instead, with the alignment itself checked by assertion. + // + // test2.sorted.bam is used rather than test.sorted.phased.bam because the + // latter's records each carry two RG tags (one from minimap2 -R, one from + // the original dorado basecall) while its header declares only one. dorado + // preserves both, so the output trips htsjdk's READ_GROUP_NOT_FOUND check. + when { + process { + """ + input[0] = [ + [ id: 'test' ], + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/nanopore/bam/test2.sorted.bam', checkIfExists: true) + ] + input[1] = [ + [ id: 'genome' ], + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta.fai', checkIfExists: true) + ] + """ + } + } + + then { + assertAll( + { assert process.success }, + { assert process.out.bam }, + // --emit-summary is set in nextflow.config, so the optional + // summary channel must be populated and correctly prefixed + { assert process.out.summary }, + { assert file(process.out.summary[0][1]).name == 'test.sequencing_summary.txt' }, + { assert path(process.out.summary[0][1]).readLines().size() > 1 }, + { assert snapshot([ + bam_name : file(process.out.bam[0][1]).name, + n_records: bam(process.out.bam[0][1]).getReads().size(), + versions : process.out.versions_dorado + ]).match() } + ) + } + } +} diff --git a/modules/nf-core/dorado/aligner/tests/main.nf.test.snap b/modules/nf-core/dorado/aligner/tests/main.nf.test.snap new file mode 100644 index 000000000000..bcda170bbf3a --- /dev/null +++ b/modules/nf-core/dorado/aligner/tests/main.nf.test.snap @@ -0,0 +1,79 @@ +{ + "homo sapiens - nanopore bam - stub": { + "content": [ + { + "0": [ + [ + { + "id": "test" + }, + "test.bam:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "1": [ + [ + { + "id": "test" + }, + "test.sequencing_summary.txt:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "2": [ + [ + "DORADO_ALIGNER", + "dorado", + "1.4.0+ba44a013" + ] + ], + "bam": [ + [ + { + "id": "test" + }, + "test.bam:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "summary": [ + [ + { + "id": "test" + }, + "test.sequencing_summary.txt:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "versions_dorado": [ + [ + "DORADO_ALIGNER", + "dorado", + "1.4.0+ba44a013" + ] + ] + } + ], + "timestamp": "2026-09-06T13:17:37.534043326", + "meta": { + "nf-test": "0.9.5", + "nextflow": "25.10.4" + } + }, + "homo sapiens - nanopore bam": { + "content": [ + { + "bam_name": "test.bam", + "n_records": 159, + "versions": [ + [ + "DORADO_ALIGNER", + "dorado", + "1.4.0+ba44a013" + ] + ] + } + ], + "timestamp": "2026-09-06T13:17:49.195776876", + "meta": { + "nf-test": "0.9.5", + "nextflow": "25.10.4" + } + } +} \ No newline at end of file diff --git a/modules/nf-core/dorado/aligner/tests/nextflow.config b/modules/nf-core/dorado/aligner/tests/nextflow.config new file mode 100644 index 000000000000..18756d21b5f6 --- /dev/null +++ b/modules/nf-core/dorado/aligner/tests/nextflow.config @@ -0,0 +1,6 @@ +process { + withName: 'DORADO_ALIGNER' { + // exercise the optional per-read summary output + ext.args = '--emit-summary' + } +}