diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ccbb4270..51bbe0795 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix [#375](https://github.com/nf-core/scrnaseq/issues/375): mismatch between index and probeset when cellranger multi is used without a prebuilt index and an FFPE probeset is passed ([#502](https://github.com/nf-core/scrnaseq/pull/502)) - Fix [#510](https://github.com/nf-core/scrnaseq/issues/510): Handle files with BOMs. ([#511](https://github.com/nf-core/scrnaseq/pull/511)) - Address [#512], adding early validation of the cellranger multi barcode sheet ([#513](https://github.com/nf-core/scrnaseq/pull/513)) +- Address [#525], adding velocyto option for the cellranger branch of the pipeline ([#525](https://github.com/nf-core/scrnaseq/pull/525)) ### Chore diff --git a/conf/modules.config b/conf/modules.config index 381de4a76..11453a2da 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -122,6 +122,19 @@ process { time = { 240.h * task.attempt } } + withName: VELOCYTO { + publishDir = [ + path: "${params.outdir}/${params.aligner}/velocyto", + mode: params.publish_dir_mode, + saveAs: { filename -> "${meta.id}/${filename}" } + ] + } + + withName: SAMTOOLS_SORT { + ext.prefix = { "cellsorted_${bam.baseName}" } + ext.args = '-t CB -O BAM' + } + withName: 'SIMPLEAF_INDEX' { publishDir = [ path: { "${params.outdir}/${params.aligner}" }, diff --git a/modules.json b/modules.json index 8cce48a16..41e4b33ff 100644 --- a/modules.json +++ b/modules.json @@ -85,6 +85,11 @@ "git_sha": "af27af1be706e6a2bb8fe454175b0cdf77f47b49", "installed_by": ["modules"] }, + "samtools/sort": { + "branch": "master", + "git_sha": "5cb9a8694da0a0e550921636bb60bc8c56445fd7", + "installed_by": ["modules"] + }, "simpleaf/index": { "branch": "master", "git_sha": "4ce8f451b1c44e02f5846b39276f5c501ca199ac", @@ -104,6 +109,11 @@ "branch": "master", "git_sha": "b0c3ff2485534c09d9debbf20125d9c6b72ce118", "installed_by": ["modules"] + }, + "velocyto": { + "branch": "master", + "git_sha": "ca3514e876459e9dd482a477a281b5b5886a1a84", + "installed_by": ["modules"] } } }, diff --git a/modules/nf-core/samtools/sort/environment.yml b/modules/nf-core/samtools/sort/environment.yml new file mode 100644 index 000000000..89e12a645 --- /dev/null +++ b/modules/nf-core/samtools/sort/environment.yml @@ -0,0 +1,10 @@ +--- +# 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/htslib + - bioconda::htslib=1.22.1 + # renovate: datasource=conda depName=bioconda/samtools + - bioconda::samtools=1.22.1 diff --git a/modules/nf-core/samtools/sort/main.nf b/modules/nf-core/samtools/sort/main.nf new file mode 100644 index 000000000..6b5aa31dd --- /dev/null +++ b/modules/nf-core/samtools/sort/main.nf @@ -0,0 +1,78 @@ +process SAMTOOLS_SORT { + tag "$meta.id" + label 'process_medium' + + conda "${moduleDir}/environment.yml" + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'https://depot.galaxyproject.org/singularity/samtools:1.22.1--h96c455f_0' : + 'biocontainers/samtools:1.22.1--h96c455f_0' }" + + input: + tuple val(meta) , path(bam) + tuple val(meta2), path(fasta) + val index_format + + output: + tuple val(meta), path("${prefix}.bam"), emit: bam, optional: true + tuple val(meta), path("${prefix}.cram"), emit: cram, optional: true + tuple val(meta), path("${prefix}.sam"), emit: sam, optional: true + tuple val(meta), path("${prefix}.${extension}.crai"), emit: crai, optional: true + tuple val(meta), path("${prefix}.${extension}.csi"), emit: csi, optional: true + tuple val(meta), path("${prefix}.${extension}.bai"), emit: bai, optional: true + tuple val("${task.process}"), val('samtools'), eval("samtools version | sed '1!d;s/.* //'"), topic: versions, emit: versions_samtools + + when: + task.ext.when == null || task.ext.when + + script: + def args = task.ext.args ?: '' + prefix = task.ext.prefix ?: "${meta.id}" + extension = args.contains("--output-fmt sam") ? "sam" : + args.contains("--output-fmt cram") ? "cram" : + "bam" + def reference = fasta ? "--reference ${fasta}" : "" + output_file = index_format ? "${prefix}.${extension}##idx##${prefix}.${extension}.${index_format} --write-index" : "${prefix}.${extension}" + if (index_format) { + if (!index_format.matches('bai|csi|crai')) { + error "Index format not one of bai, csi, crai." + } else if (extension == "sam") { + error "Indexing not compatible with SAM output" + } + } + if ("$bam" == "${prefix}.bam") error "Input and output names are the same, use \"task.ext.prefix\" to disambiguate!" + + """ + samtools cat \\ + ${bam} \\ + | \\ + samtools sort \\ + $args \\ + -T ${prefix} \\ + --threads $task.cpus \\ + ${reference} \\ + -o ${output_file} \\ + - + + """ + + stub: + def args = task.ext.args ?: '' + prefix = task.ext.prefix ?: "${meta.id}" + extension = args.contains("--output-fmt sam") ? "sam" : + args.contains("--output-fmt cram") ? "cram" : + "bam" + if (index_format) { + if (!index_format.matches('bai|csi|crai')) { + error "Index format not one of bai, csi, crai." + } else if (extension == "sam") { + error "Indexing not compatible with SAM output" + } + } + index = index_format ? "touch ${prefix}.${extension}.${index_format}" : "" + + """ + touch ${prefix}.${extension} + ${index} + + """ +} diff --git a/modules/nf-core/samtools/sort/meta.yml b/modules/nf-core/samtools/sort/meta.yml new file mode 100644 index 000000000..699683047 --- /dev/null +++ b/modules/nf-core/samtools/sort/meta.yml @@ -0,0 +1,142 @@ +name: samtools_sort +description: Sort SAM/BAM/CRAM file +keywords: + - sort + - 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 ] + - bam: + type: file + description: BAM/CRAM/SAM file(s) + pattern: "*.{bam,cram,sam}" + ontologies: [] + - - meta2: + type: map + description: | + Groovy Map containing reference information + e.g. [ id:'genome' ] + - fasta: + type: file + description: Reference genome FASTA file + pattern: "*.{fa,fasta,fna}" + optional: true + ontologies: [] + - index_format: + type: string + description: Index format to use (optional) + pattern: "bai|csi|crai" +output: + bam: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - "${prefix}.bam": + type: file + description: Sorted BAM file + pattern: "*.{bam}" + ontologies: [] + cram: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - "${prefix}.cram": + type: file + description: Sorted CRAM file + pattern: "*.{cram}" + ontologies: [] + sam: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - "${prefix}.sam": + type: file + description: Sorted SAM file + pattern: "*.{sam}" + ontologies: [] + crai: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - "${prefix}.${extension}.crai": + type: file + description: CRAM index file (optional) + pattern: "*.crai" + ontologies: [] + csi: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - "${prefix}.${extension}.csi": + type: file + description: BAM index file (optional) + pattern: "*.csi" + ontologies: [] + bai: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'test', single_end:false ] + - "${prefix}.${extension}.bai": + type: file + description: BAM index file (optional) + pattern: "*.bai" + ontologies: [] + versions_samtools: + - - ${task.process}: + type: string + description: The process the versions were collected from + - samtools: + type: string + description: The tool name + - "samtools version | sed '1!d;s/.* //'": + type: string + description: The command used to generate the version of the tool + +topics: + versions: + - - ${task.process}: + type: string + description: The process the versions were collected from + - samtools: + type: string + description: The tool name + - "samtools version | sed '1!d;s/.* //'": + type: string + description: The command used to generate the version of the tool + +authors: + - "@drpatelh" + - "@ewels" + - "@matthdsm" +maintainers: + - "@drpatelh" + - "@ewels" + - "@matthdsm" diff --git a/modules/nf-core/samtools/sort/tests/main.nf.test b/modules/nf-core/samtools/sort/tests/main.nf.test new file mode 100644 index 000000000..df47bb25c --- /dev/null +++ b/modules/nf-core/samtools/sort/tests/main.nf.test @@ -0,0 +1,332 @@ +nextflow_process { + + name "Test Process SAMTOOLS_SORT" + script "../main.nf" + process "SAMTOOLS_SORT" + tag "modules" + tag "modules_nfcore" + tag "samtools" + tag "samtools/sort" + + test("bam_no_index") { + + config "./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.bam', checkIfExists: true) + ]) + input[1] = Channel.of([ + [ id:'fasta' ], // meta map + file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta', checkIfExists: true) + ]) + input[2] = '' + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot( + process.out.bam, + process.out.bai, + process.out.findAll { key, val -> key.startsWith("versions") } + ).match()} + ) + } + } + + test("bam_bai_index") { + + config "./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.bam', checkIfExists: true) + ]) + input[1] = Channel.of([ + [ id:'fasta' ], // meta map + file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta', checkIfExists: true) + ]) + input[2] = 'bai' + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot( + process.out.bam, + process.out.bai, + process.out.findAll { key, val -> key.startsWith("versions") } + ).match()} + ) + } + } + + test("bam_csi_index") { + + config "./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.bam', checkIfExists: true) + ]) + input[1] = Channel.of([ + [ id:'fasta' ], // meta map + file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta', checkIfExists: true) + ]) + input[2] = 'csi' + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot( + process.out.bam, + process.out.csi, + process.out.findAll { key, val -> key.startsWith("versions") } + ).match()} + ) + } + } + + test("multiple bam") { + + config "./nextflow.config" + + when { + process { + """ + input[0] = Channel.of([ + [ id:'test', single_end:false ], // meta map + [ + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/bam/test.paired_end.sorted.bam', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/bam/test2.paired_end.sorted.bam', checkIfExists: true) + ] + ]) + input[1] = Channel.of([ + [ id:'fasta' ], // meta map + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta', checkIfExists: true) + ]) + input[2] = '' + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot( + process.out.bam, + process.out.csi.collect { it.collect { it instanceof Map ? it : file(it).name } }, + process.out.findAll { key, val -> key.startsWith("versions") } + ).match()} + ) + } + } + + test("multiple bam bai index") { + + config "./nextflow.config" + + when { + process { + """ + input[0] = Channel.of([ + [ id:'test', single_end:false ], // meta map + [ + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/bam/test.paired_end.sorted.bam', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/bam/test2.paired_end.sorted.bam', checkIfExists: true) + ] + ]) + input[1] = Channel.of([ + [ id:'fasta' ], // meta map + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta', checkIfExists: true) + ]) + input[2] = 'bai' + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot( + process.out.bam, + process.out.bai.collect { it.collect { it instanceof Map ? it : file(it).name } }, + process.out.findAll { key, val -> key.startsWith("versions") } + ).match()} + ) + } + } + + test("multiple bam csi index") { + + config "./nextflow.config" + + when { + process { + """ + input[0] = Channel.of([ + [ id:'test', single_end:false ], // meta map + [ + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/bam/test.paired_end.sorted.bam', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/bam/test2.paired_end.sorted.bam', checkIfExists: true) + ] + ]) + input[1] = Channel.of([ + [ id:'fasta' ], // meta map + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta', checkIfExists: true) + ]) + input[2] = 'csi' + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot( + process.out.bam, + process.out.csi.collect { it.collect { it instanceof Map ? it : file(it).name } }, + process.out.findAll { key, val -> key.startsWith("versions") } + ).match()} + ) + } + } + + test("cram") { + + config "./nextflow_cram.config" + + 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.sorted.cram', checkIfExists: true) + ]) + input[1] = Channel.of([ + [ id:'fasta' ], // meta map + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta', checkIfExists: true) + ]) + input[2] = '' + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot( + process.out.cram.collect { it.collect { it instanceof Map ? it : file(it).name } }, + process.out.crai.collect { it.collect { it instanceof Map ? it : file(it).name } }, + process.out.findAll { key, val -> key.startsWith("versions") } + ).match()} + ) + } + } + + test("bam - stub") { + + options "-stub" + config "./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.bam', checkIfExists: true) + ]) + input[1] = Channel.of([ + [ id:'fasta' ], // meta map + file(params.modules_testdata_base_path + 'genomics/sarscov2/genome/genome.fasta', checkIfExists: true) + ]) + input[2] = '' + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot(process.out.findAll { key, val -> key.startsWith("versions") }).match() } + ) + } + } + + test("multiple bam - stub") { + + config "./nextflow.config" + + when { + process { + """ + input[0] = Channel.of([ + [ id:'test', single_end:false ], // meta map + [ + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/bam/test.paired_end.sorted.bam', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/illumina/bam/test2.paired_end.sorted.bam', checkIfExists: true) + ] + ]) + input[1] = Channel.of([ + [ id:'fasta' ], // meta map + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta', checkIfExists: true) + ]) + input[2] = '' + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot(process.out.findAll { key, val -> key.startsWith("versions") }).match() } + ) + } + } + + test("cram - stub") { + + options "-stub" + config "./nextflow_cram.config" + + 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.sorted.cram', checkIfExists: true) + ]) + input[1] = Channel.of([ + [ id:'fasta' ], // meta map + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta', checkIfExists: true) + ]) + input[2] = '' + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot(process.out.findAll { key, val -> key.startsWith("versions") }).match() } + ) + } + } +} diff --git a/modules/nf-core/samtools/sort/tests/main.nf.test.snap b/modules/nf-core/samtools/sort/tests/main.nf.test.snap new file mode 100644 index 000000000..4e618fa3f --- /dev/null +++ b/modules/nf-core/samtools/sort/tests/main.nf.test.snap @@ -0,0 +1,296 @@ +{ + "cram": { + "content": [ + [ + [ + { + "id": "test", + "single_end": false + }, + "test.sorted.cram" + ] + ], + [ + [ + { + "id": "test", + "single_end": false + }, + "test.sorted.cram.crai" + ] + ], + { + "versions_samtools": [ + [ + "SAMTOOLS_SORT", + "samtools", + "1.22.1" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.0" + }, + "timestamp": "2025-10-29T12:47:01.171084" + }, + "bam_csi_index": { + "content": [ + [ + [ + { + "id": "test", + "single_end": false + }, + "test.sorted.bam:md5,72ca1dff5344a5e5e6b892fe5f6b134d" + ] + ], + [ + [ + { + "id": "test", + "single_end": false + }, + "test.sorted.bam.csi:md5,01394e702c729cb478df914ffaf9f7f8" + ] + ], + { + "versions_samtools": [ + [ + "SAMTOOLS_SORT", + "samtools", + "1.22.1" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.0" + }, + "timestamp": "2025-10-29T12:46:00.961675" + }, + "bam - stub": { + "content": [ + { + "versions_samtools": [ + [ + "SAMTOOLS_SORT", + "samtools", + "1.22.1" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.0" + }, + "timestamp": "2025-10-29T12:47:12.154354" + }, + "multiple bam bai index": { + "content": [ + [ + [ + { + "id": "test", + "single_end": false + }, + "test.sorted.bam:md5,3ffa2affc29f0aa6e7b36dded84625fe" + ] + ], + [ + [ + { + "id": "test", + "single_end": false + }, + "test.sorted.bam.bai" + ] + ], + { + "versions_samtools": [ + [ + "SAMTOOLS_SORT", + "samtools", + "1.22.1" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.0" + }, + "timestamp": "2025-10-29T12:46:25.488622" + }, + "cram - stub": { + "content": [ + { + "versions_samtools": [ + [ + "SAMTOOLS_SORT", + "samtools", + "1.22.1" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.0" + }, + "timestamp": "2025-10-29T12:47:28.485045" + }, + "multiple bam": { + "content": [ + [ + [ + { + "id": "test", + "single_end": false + }, + "test.sorted.bam:md5,cd4eb0077f25e9cff395366b8883dd1f" + ] + ], + [ + + ], + { + "versions_samtools": [ + [ + "SAMTOOLS_SORT", + "samtools", + "1.22.1" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.0" + }, + "timestamp": "2025-10-29T12:46:13.168476" + }, + "multiple bam - stub": { + "content": [ + { + "versions_samtools": [ + [ + "SAMTOOLS_SORT", + "samtools", + "1.22.1" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.0" + }, + "timestamp": "2025-10-29T12:47:21.628088" + }, + "bam_no_index": { + "content": [ + [ + [ + { + "id": "test", + "single_end": false + }, + "test.sorted.bam:md5,26b27d1f9bcb61c25da21b562349784e" + ] + ], + [ + + ], + { + "versions_samtools": [ + [ + "SAMTOOLS_SORT", + "samtools", + "1.22.1" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.0" + }, + "timestamp": "2025-10-29T12:45:47.139418" + }, + "multiple bam csi index": { + "content": [ + [ + [ + { + "id": "test", + "single_end": false + }, + "test.sorted.bam:md5,295503ba5342531a3310c33ad0efbc22" + ] + ], + [ + [ + { + "id": "test", + "single_end": false + }, + "test.sorted.bam.csi" + ] + ], + { + "versions_samtools": [ + [ + "SAMTOOLS_SORT", + "samtools", + "1.22.1" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.0" + }, + "timestamp": "2025-10-29T12:46:51.5531" + }, + "bam_bai_index": { + "content": [ + [ + [ + { + "id": "test", + "single_end": false + }, + "test.sorted.bam:md5,cae7564cb83bb4a5911205bf94124b54" + ] + ], + [ + [ + { + "id": "test", + "single_end": false + }, + "test.sorted.bam.bai:md5,50dd467c169545a4d5d1f709f7e986e0" + ] + ], + { + "versions_samtools": [ + [ + "SAMTOOLS_SORT", + "samtools", + "1.22.1" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.3", + "nextflow": "25.10.0" + }, + "timestamp": "2025-10-29T12:45:52.796936" + } +} \ No newline at end of file diff --git a/modules/nf-core/samtools/sort/tests/nextflow.config b/modules/nf-core/samtools/sort/tests/nextflow.config new file mode 100644 index 000000000..723f62b21 --- /dev/null +++ b/modules/nf-core/samtools/sort/tests/nextflow.config @@ -0,0 +1,7 @@ +process { + + withName: SAMTOOLS_SORT { + ext.prefix = { "${meta.id}.sorted" } + } + +} diff --git a/modules/nf-core/samtools/sort/tests/nextflow_cram.config b/modules/nf-core/samtools/sort/tests/nextflow_cram.config new file mode 100644 index 000000000..3a8c0188b --- /dev/null +++ b/modules/nf-core/samtools/sort/tests/nextflow_cram.config @@ -0,0 +1,8 @@ +process { + + withName: SAMTOOLS_SORT { + ext.prefix = { "${meta.id}.sorted" } + ext.args = "--write-index --output-fmt cram" + } + +} diff --git a/modules/nf-core/velocyto/environment.yml b/modules/nf-core/velocyto/environment.yml new file mode 100644 index 000000000..f0a06a20d --- /dev/null +++ b/modules/nf-core/velocyto/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::velocyto.py=0.17.17 diff --git a/modules/nf-core/velocyto/main.nf b/modules/nf-core/velocyto/main.nf new file mode 100644 index 000000000..0ad3cc593 --- /dev/null +++ b/modules/nf-core/velocyto/main.nf @@ -0,0 +1,46 @@ +process VELOCYTO { + tag "$meta.id" + label 'process_medium' + + conda "${moduleDir}/environment.yml" + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + 'https://depot.galaxyproject.org/singularity/velocyto.py:0.17.17--py38h24c8ff8_6': + 'biocontainers/velocyto.py:0.17.17--py38h24c8ff8_6' }" + + stageInMode 'copy' + + input: + tuple val(meta), path(barcodes), path(bam), path(sorted_bam) + path gtf + + output: + tuple val(meta), path("*.loom"), path("*.velocyto.log"), emit: loom + 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}" + """ + velocyto run $args -e ${meta.id} -b ${barcodes} -o . ${bam} ${gtf} > ${prefix}.velocyto.log + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + velocyto: \$(echo \$(velocyto --version) | sed 's/^.*version //') + END_VERSIONS + """ + + stub: + def prefix = task.ext.prefix ?: "${meta.id}" + """ + touch ${prefix}.loom + touch ${prefix}.velocyto.log + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + velocyto: \$(echo \$(velocyto --version) | sed 's/^.*version //') + END_VERSIONS + """ +} diff --git a/modules/nf-core/velocyto/meta.yml b/modules/nf-core/velocyto/meta.yml new file mode 100644 index 000000000..53ccf96c1 --- /dev/null +++ b/modules/nf-core/velocyto/meta.yml @@ -0,0 +1,99 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/meta-schema.json +name: "velocyto" +description: | + Velocyto is a library for the analysis of RNA velocity. velocyto.py CLI use + `Path(resolve_path=True)` and breaks the nextflow logic of symbolic links. + If in the work dir velocyto find a file named EXACTLY `cellsorted_[ORIGINAL_BAM_NAME]` + it will skip the samtools sort step. + Cellsorted bam file should be cell sorted with: + ```bash + samtools sort -t CB -O BAM -o cellsorted_input.bam input.bam + ``` + See module test for an example with the SAMTOOLS_SORT nf-core module. + Config example to cellsort input bam using SAMTOOLS_SORT: + ```groovy + withName: SAMTOOLS_SORT { + ext.prefix = { "cellsorted_${bam.baseName}" } + ext.args = '-t CB -O BAM' + } + ``` + Optional mask must be passed with `ext.args` and option `--mask` + This is why I need to stage in the work dir 2 bam files (cellsorted and original). + See also [velocyto tutorial](https://velocyto.org/velocyto.py/tutorial/cli.html#notes-on-first-runtime-and-parallelization) + +keywords: + - count + - rnaseq + - rna velocity + - bam + +tools: + - velocyto: + description: A library for the analysis of RNA velocity. + homepage: https://github.com/velocyto-team/velocyto.py + documentation: https://velocyto.org/velocyto.py + tool_dev_url: https://github.com/velocyto-team/velocyto.py + doi: "10.1038/s41586-018-0414-6" + licence: ["MIT"] + identifier: "" + +input: + # Only when we have meta + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'sample1' ]` + - barcodes: + type: file + description: Valid barcodes file, to filter the bam + pattern: "*.tsv.gz" + ontologies: + - edam: http://edamontology.org/format_3989 # GZIP format + - bam: + type: file + description: Sorted BAM/CRAM/SAM file + pattern: "*.{bam,cram,sam}" + ontologies: [] + - sorted_bam: + type: file + description: Cell sorted BAM/CRAM/SAM file generated with `samtools sort -t + CB -O BAM -o cellsorted_possorted_genome_bam.bam possorted_genome_bam.bam` + pattern: "*.bam" + ontologies: [] + - gtf: + type: file + description: genome annotation file + pattern: "*.gtf" + ontologies: [] +output: + #Only when we have meta + loom: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'sample1' ]` + - "*.loom": + type: file + description: Loom file with counts divided in spliced/unspliced/ambiguous. + pattern: "*.loom" + ontologies: + - edam: http://edamontology.org/format_3913 # Loom + - "*.velocyto.log": + type: file + description: Loom file with counts divided in spliced/unspliced/ambiguous. + pattern: "*.loom" + ontologies: + - edam: http://edamontology.org/format_3913 # Loom + versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" + ontologies: + - edam: http://edamontology.org/format_3750 # YAML +authors: + - "@tucano" +maintainers: + - "@tucano" diff --git a/modules/nf-core/velocyto/tests/main.nf.test b/modules/nf-core/velocyto/tests/main.nf.test new file mode 100644 index 000000000..a5df292eb --- /dev/null +++ b/modules/nf-core/velocyto/tests/main.nf.test @@ -0,0 +1,141 @@ +// nf-core modules test velocyto +nextflow_process { + name 'Test Process VELOCYTO' + tag "modules" + tag "modules_nfcore" + tag "velocyto" + tag "samtools/sort" + + script '../main.nf' + process 'VELOCYTO' + config "./test.config" + + test('mus_musculus - barcodes and bam with samtools sort') { + setup { + run("SAMTOOLS_SORT") { + script "../../samtools/sort/main.nf" + process { + """ + input[0] = [ + [ id:'Sample_X' ], // meta map + file(params.modules_testdata_base_path + 'genomics/mus_musculus/rna_velocity/possorted_genome_bam.bam', checkIfExists: true), + ] + input[1] = [ + [ id:'Sample_X' ], // meta map + file(params.modules_testdata_base_path + 'genomics/mus_musculus/genome/chr19.fa.gz', checkIfExists: true) + ] + input[2] = '' + """ + } + } + } + + when { + process { + """ + input[0] = Channel + .fromList( + [ + [ id:'Sample_X' ], // meta map + file(params.modules_testdata_base_path + 'genomics/mus_musculus/rna_velocity/barcodes.tsv.gz', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/mus_musculus/rna_velocity/possorted_genome_bam.bam', checkIfExists: true), + ] + ) + .collect() + .join(SAMTOOLS_SORT.out.bam) + + input[1] = file(params.modules_testdata_base_path + 'genomics/mus_musculus/rna_velocity/gencode.vM19.annotation.chr19.gtf', checkIfExists: true) + """ + } + } + then { + assertAll( + { assert process.success }, + { + assert snapshot( + process.out.versions + ).match("version") + }, + // this is to check if velocyto is correctly skipping the samtools sort part + { + with(process.out.loom) { + with(get(0)) { + assert path(get(2)).readLines().any { + it.contains('cellsorted_possorted_genome_bam.bam already exists') + } + } + } + }, + { assert process.out.loom.get(0).get(1) ==~ ".*/Sample_X.loom" } + ) + } + } + + test('mus_musculus - barcodes, bam and bam cellsorted') { + when { + process { + """ + input[0] = [ + [ id:'Sample_X' ], // meta map + file(params.modules_testdata_base_path + 'genomics/mus_musculus/rna_velocity/barcodes.tsv.gz', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/mus_musculus/rna_velocity/possorted_genome_bam.bam', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/mus_musculus/rna_velocity/cellsorted_possorted_genome_bam.bam', checkIfExists: true) + ] + + input[1] = file(params.modules_testdata_base_path + 'genomics/mus_musculus/rna_velocity/gencode.vM19.annotation.chr19.gtf', checkIfExists: true) + """ + } + } + + then { + assertAll( + { assert process.success }, + { + assert snapshot( + process.out.versions + ).match("presorted_version") + }, + // this is to check if velocyto is correctly skipping the samtools sort part + { + with(process.out.loom) { + with(get(0)) { + assert path(get(2)).readLines().any { + it.contains('cellsorted_possorted_genome_bam.bam already exists') + } + } + } + }, + { assert process.out.loom.get(0).get(1) ==~ ".*/Sample_X.loom" } + ) + } + } + + test('mus_musculus - barcodes and bams - stub') { + options '-stub' + when { + process { + """ + input[0] = [ + [ id:'Sample_X' ], // meta map + file(params.modules_testdata_base_path + 'genomics/mus_musculus/rna_velocity/barcodes.tsv.gz', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/mus_musculus/rna_velocity/possorted_genome_bam.bam', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/mus_musculus/rna_velocity/cellsorted_possorted_genome_bam.bam', checkIfExists: true) + ] + input[1] = file(params.modules_testdata_base_path + 'genomics/mus_musculus/rna_velocity/gencode.vM19.annotation.chr19.gtf', checkIfExists: true) + """ + } + } + + then { + assertAll( + { assert process.success }, + { + assert snapshot( + process.out.versions, + process.out.loom + ).match() + }, + ) + } + } +} diff --git a/modules/nf-core/velocyto/tests/main.nf.test.snap b/modules/nf-core/velocyto/tests/main.nf.test.snap new file mode 100644 index 000000000..de0052e1d --- /dev/null +++ b/modules/nf-core/velocyto/tests/main.nf.test.snap @@ -0,0 +1,47 @@ +{ + "mus_musculus - barcodes and bams - stub": { + "content": [ + [ + "versions.yml:md5,afb422608e1df855e73243985d23f34d" + ], + [ + [ + { + "id": "Sample_X" + }, + "Sample_X.loom:md5,d41d8cd98f00b204e9800998ecf8427e", + "Sample_X.velocyto.log:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ] + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-03-22T12:34:10.323349" + }, + "presorted_version": { + "content": [ + [ + "versions.yml:md5,afb422608e1df855e73243985d23f34d" + ] + ], + "meta": { + "nf-test": "0.8.4", + "nextflow": "23.10.1" + }, + "timestamp": "2024-03-22T12:33:34.369237" + }, + "version": { + "content": [ + [ + "versions.yml:md5,afb422608e1df855e73243985d23f34d" + ] + ], + "meta": { + "nf-test": "0.9.2", + "nextflow": "25.04.6" + }, + "timestamp": "2025-08-13T14:05:02.982149497" + } +} \ No newline at end of file diff --git a/modules/nf-core/velocyto/tests/test.config b/modules/nf-core/velocyto/tests/test.config new file mode 100644 index 000000000..66a13319c --- /dev/null +++ b/modules/nf-core/velocyto/tests/test.config @@ -0,0 +1,6 @@ +process { + withName: SAMTOOLS_SORT { + ext.prefix = { "cellsorted_${bam.baseName}" } + ext.args = '-t CB -O BAM' + } +} diff --git a/nextflow.config b/nextflow.config index 81cf3599f..56bd2afec 100644 --- a/nextflow.config +++ b/nextflow.config @@ -41,6 +41,9 @@ params { skip_cellranger_renaming = false cellranger_index = null + // Velocyto parameters + run_velocyto = false + // Cellranger ARC parameters motifs = null cellrangerarc_config = null @@ -61,7 +64,6 @@ params { cellranger_multi_barcodes = null gex_reference_version = null - // Template Boilerplate options skip_multiqc = false diff --git a/nextflow_schema.json b/nextflow_schema.json index 41f6cff10..82c80a6d5 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -285,6 +285,10 @@ "skip_cellranger_renaming": { "type": "boolean", "description": "Should it skip the automatic renaming included in cellranger-related modules?" + }, + "run_velocyto": { + "type": "boolean", + "description": "Run velocyto from the bam output." } } }, diff --git a/subworkflows/local/align_cellranger.nf b/subworkflows/local/align_cellranger.nf index d787e0f00..5e1e5b904 100644 --- a/subworkflows/local/align_cellranger.nf +++ b/subworkflows/local/align_cellranger.nf @@ -5,6 +5,9 @@ include { CELLRANGER_MKGTF } from "../../modules/nf-core/cellranger/mkgtf/main.nf" include { CELLRANGER_MKREF } from "../../modules/nf-core/cellranger/mkref/main.nf" include { CELLRANGER_COUNT } from "../../modules/nf-core/cellranger/count/main.nf" +// Modules for Velocyto launch: +include { VELOCYTO } from "../../modules/nf-core/velocyto/main.nf" +include { SAMTOOLS_SORT } from '../../modules/nf-core/samtools/sort/main' // Define workflow to subset and index a genome region fasta file workflow CELLRANGER_ALIGN { @@ -61,6 +64,50 @@ workflow CELLRANGER_ALIGN { [ meta + [input_type: 'filtered'], desired_files ] } + // Run Velocyto on the output if requested by --run_velocyto true: + if ( params.run_velocyto ) { + // Extract the two Cell Ranger files that VELOCYTO needs: + ch_velocyto_files = + CELLRANGER_COUNT.out.outs + .map { meta, cellranger_output_files -> + + def bam = cellranger_output_files.find { + it.toString().endsWith('/possorted_genome_bam.bam') + } + + def barcodes = cellranger_output_files.find { + it.toString().endsWith('/filtered_feature_bc_matrix/barcodes.tsv.gz') + } + + tuple(meta, barcodes, bam) + } + + // SAMTOOLS_SORT requires this extra input (index, optional). + ch_no_index = Channel.value('') + ch_fasta_for_sort = fasta.map { fa -> tuple([id: 'genome'], fa) } + + // Create cellsorted_possorted_genome_bam.bam + SAMTOOLS_SORT( + ch_velocyto_files.map { meta, barcodes, bam -> tuple(meta, bam) }, + ch_fasta_for_sort, + ch_no_index + ) + + // Recombine into the exact tuple VELOCYTO expects + ch_velocyto_input = + ch_velocyto_files + .join(SAMTOOLS_SORT.out.bam) + .map { meta, barcodes, bam, sorted_bam -> + tuple(meta + [input_type: 'velocyto'], barcodes, bam, sorted_bam) + } + + VELOCYTO( + ch_velocyto_input, + gtf + ) + ch_versions = ch_versions.mix(VELOCYTO.out.versions) + } + emit: ch_versions cellranger_out = CELLRANGER_COUNT.out.outs