diff --git a/assets/catalog/catalog.md b/assets/catalog/catalog.md index 452203e..d6e96d0 100644 --- a/assets/catalog/catalog.md +++ b/assets/catalog/catalog.md @@ -100,6 +100,12 @@ - [spliceai/derive](../../modules/nextflow/spliceai/derive/main.nf) — Derive splice event scores from SpliceAI predictions. - [spliceai/predict](../../modules/nextflow/spliceai/predict/main.nf) — Predict splice junctions from genomic sequences using SpliceAI. - [spliceai/publish](../../modules/nextflow/spliceai/publish/main.nf) — Collect and organize SpliceAI output files. +- [spliceai2/chunk](../../modules/nextflow/spliceai2/chunk/main.nf) — Chunk genomic sequences for parallel SpliceAI2 prediction. +- [spliceai2/predict](../../modules/nextflow/spliceai2/predict/main.nf) — Predict splice sites from genomic sequences using SpliceAI2. +- [spliceai2/publish](../../modules/nextflow/spliceai2/publish/main.nf) — Collect and organize SpliceAI2 output files. +- [spliceai2/transcripts](../../modules/nextflow/spliceai2/transcripts/main.nf) — Decode transcript structures for a set of loci using SpliceAI2. +- [subworkflows/spliceai2](../../subworkflows/spliceai2/main.nf) — Chunk, predict, convert, merge, and optionally decode SpliceAI2 outputs. +- [workflows/spliceai2](../../workflows/spliceai2/main.nf) — Standalone SpliceAI2 pipeline. - [aparent/chunk](../../modules/nextflow/aparent/chunk/main.nf) — Chunk genomic regions for parallel APARENT prediction. - [aparent/predict](../../modules/nextflow/aparent/predict/main.nf) — Predict polyadenylation sites using the APARENT deep learning model. - [minisplice/download](../../modules/nextflow/minisplice/download/main.nf) — Download MiniSplice model and calibration files. diff --git a/modules/nextflow/spliceai2/chunk/main.nf b/modules/nextflow/spliceai2/chunk/main.nf new file mode 100644 index 0000000..6ad7a0b --- /dev/null +++ b/modules/nextflow/spliceai2/chunk/main.nf @@ -0,0 +1,59 @@ +/* +Copyright (c) 2026 The Hiller Lab at the Senckenberg Gessellschaft für Naturforschung +Distributed under the terms of the Apache License, Version 2.0. +*/ + +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + SPLICEAI2_CHUNK — Chunk genomic sequences for parallel SpliceAI2 prediction. +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +*/ + +process SPLICEAI2_CHUNK { + tag "$meta.id" + label 'process_low' + + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + '' : + 'ghcr.io/hillerlab/spliceai2:latest' }" + + input: + tuple val(meta), path(genome) + val compression + + output: + tuple val(meta), path("chunks/*.fa"), optional: true, emit: fasta + tuple val(meta), path("chunks/*.fa.gz"), optional: true, emit: fasta_gz + path "versions.yml", emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + def args = task.ext.args ?: '' + def gzip = compression ? '--gzip' : '' + """ + spliceai2 chunk \\ + $args \\ + $gzip \\ + --threads ${task.cpus} \\ + --sequence "${genome}" + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + spliceai2 chunk: \$( spliceai2 chunk --version | sed 's/^spliceai-chunk //g' ) + END_VERSIONS + """ + + stub: + def extension = compression ? 'fa.gz' : 'fa' + """ + mkdir -p chunks + touch chunks/stub.${extension} + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + spliceai2 chunk: \$( spliceai2 chunk --version | sed 's/^spliceai-chunk //g' ) + END_VERSIONS + """ +} diff --git a/modules/nextflow/spliceai2/predict/main.nf b/modules/nextflow/spliceai2/predict/main.nf new file mode 100644 index 0000000..e983b56 --- /dev/null +++ b/modules/nextflow/spliceai2/predict/main.nf @@ -0,0 +1,64 @@ +/* +Copyright (c) 2026 The Hiller Lab at the Senckenberg Gessellschaft für Naturforschung +Distributed under the terms of the Apache License, Version 2.0. +*/ + +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + SPLICEAI2_PREDICT — Predict splice sites for one sequence chunk. +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +*/ + +process SPLICEAI2_PREDICT { + tag "$meta.id" + label 'process_low' + label 'process_gpu' + + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + '' : + 'ghcr.io/hillerlab/spliceai2:latest' }" + + input: + tuple val(meta), path(fasta) + path models + val species + + output: + tuple val(meta), path("spliceai/*.donor_plus.wig"), emit: donor_plus + tuple val(meta), path("spliceai/*.donor_minus.wig"), emit: donor_minus + tuple val(meta), path("spliceai/*.acceptor_plus.wig"), emit: acceptor_plus + tuple val(meta), path("spliceai/*.acceptor_minus.wig"), emit: acceptor_minus + tuple val(meta), path("spliceai/*.wig"), emit: all + path "versions.yml", emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + def args = task.ext.args ?: '' + """ + spliceai2 predict --no-compile \\ + $args \\ + --sequence "${fasta}" \\ + --models "${models}" \\ + --species "${species}" \\ + --outdir spliceai + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + spliceai2: \$( spliceai2 --version | sed 's/^spliceai2 //g' ) + END_VERSIONS + """ + + stub: + """ + mkdir -p spliceai + touch spliceai/stub.donor_plus.wig spliceai/stub.donor_minus.wig \\ + spliceai/stub.acceptor_plus.wig spliceai/stub.acceptor_minus.wig + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + spliceai2: \$( spliceai2 --version | sed 's/^spliceai2 //g' ) + END_VERSIONS + """ +} diff --git a/modules/nextflow/spliceai2/publish/main.nf b/modules/nextflow/spliceai2/publish/main.nf new file mode 100644 index 0000000..affe72f --- /dev/null +++ b/modules/nextflow/spliceai2/publish/main.nf @@ -0,0 +1,40 @@ +/* +Copyright (c) 2026 The Hiller Lab at the Senckenberg Gessellschaft für Naturforschung +Distributed under the terms of the Apache License, Version 2.0. +*/ + +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + SPLICEAI2_PUBLISH — Collect the four SpliceAI2 strand and site BigWigs. +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +*/ + +process SPLICEAI2_PUBLISH { + tag "publish" + label 'process_single' + + input: + tuple val(meta), path(donor_plus) + tuple val(meta1), path(donor_minus) + tuple val(meta2), path(acceptor_plus) + tuple val(meta3), path(acceptor_minus) + + output: + path("spliceai2"), emit: spliceai2 + + script: + """ + mkdir -p spliceai2 + cp "${donor_plus}" spliceai2/ + cp "${donor_minus}" spliceai2/ + cp "${acceptor_plus}" spliceai2/ + cp "${acceptor_minus}" spliceai2/ + """ + + stub: + """ + mkdir -p spliceai2 + touch spliceai2/donor_plus.bw spliceai2/donor_minus.bw \\ + spliceai2/acceptor_plus.bw spliceai2/acceptor_minus.bw + """ +} diff --git a/modules/nextflow/spliceai2/transcripts/main.nf b/modules/nextflow/spliceai2/transcripts/main.nf new file mode 100644 index 0000000..33b7bed --- /dev/null +++ b/modules/nextflow/spliceai2/transcripts/main.nf @@ -0,0 +1,64 @@ +/* +Copyright (c) 2026 The Hiller Lab at the Senckenberg Gessellschaft für Naturforschung +Distributed under the terms of the Apache License, Version 2.0. +*/ + +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + SPLICEAI2_TRANSCRIPTS — Decode transcript structures for a set of loci. +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +*/ + +process SPLICEAI2_TRANSCRIPTS { + tag "$meta.id" + label 'process_high' + label 'process_gpu' + + container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? + '' : + 'ghcr.io/hillerlab/spliceai2:latest' }" + + input: + tuple val(meta), path(genome), path(genome_fai), path(loci) + path models + val species + + output: + tuple val(meta), path("transcripts/*.transcripts.bed"), emit: transcripts + tuple val(meta), path("transcripts/*.skipped.bed"), emit: skipped + path "versions.yml", emit: versions + + when: + task.ext.when == null || task.ext.when + + script: + def args = task.ext.args ?: '' + """ + test -s "${genome_fai}" + + spliceai2 transcripts --no-compile \\ + $args \\ + --genome "${genome}" \\ + --loci "${loci}" \\ + --models "${models}" \\ + --species "${species}" \\ + --outdir transcripts + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + spliceai2: \$( spliceai2 --version | sed 's/^spliceai2 //g' ) + END_VERSIONS + """ + + stub: + def prefix = meta.id + """ + mkdir -p transcripts + touch "transcripts/${prefix}.transcripts.bed" "transcripts/${prefix}.skipped.bed" + + cat <<-END_VERSIONS > versions.yml + "${task.process}": + spliceai2: \$( spliceai2 --version | sed 's/^spliceai2 //g' ) + END_VERSIONS + """ +} diff --git a/modules/nextflow/splicetransformer/derive/main.nf b/modules/nextflow/splicetransformer/derive/main.nf index 0db9b8d..dbaace5 100644 --- a/modules/nextflow/splicetransformer/derive/main.nf +++ b/modules/nextflow/splicetransformer/derive/main.nf @@ -46,7 +46,7 @@ process SPLICETRANSFORMER_DERIVE { cat <<-END_VERSIONS > versions.yml "${task.process}": - splicetransformer derive: $( splicetransformer derive --version | sed 's/splicetransformer derive //g' ) + splicetransformer derive: \$( splicetransformer derive --version | sed 's/splicetransformer derive //g' ) END_VERSIONS """ @@ -56,7 +56,7 @@ process SPLICETRANSFORMER_DERIVE { cat <<-END_VERSIONS > versions.yml "${task.process}": - splicetransformer derive: $( splicetransformer derive --version | sed 's/splicetransformer derive //g' ) + splicetransformer derive: \$( splicetransformer derive --version | sed 's/splicetransformer derive //g' ) END_VERSIONS """ } diff --git a/subworkflows/spliceai2/main.nf b/subworkflows/spliceai2/main.nf new file mode 100644 index 0000000..bf181c5 --- /dev/null +++ b/subworkflows/spliceai2/main.nf @@ -0,0 +1,122 @@ +/* +Copyright (c) 2026 The Hiller Lab at the Senckenberg Gessellschaft für Naturforschung +Distributed under the terms of the Apache License, Version 2.0. +*/ + +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + IMPORT LOCAL MODULES/SUBWORKFLOWS +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +*/ + +include { SPLICEAI2_CHUNK } from '../../modules/nextflow/spliceai2/chunk/main.nf' +include { SPLICEAI2_PREDICT } from '../../modules/nextflow/spliceai2/predict/main.nf' +include { SPLICEAI2_TRANSCRIPTS } from '../../modules/nextflow/spliceai2/transcripts/main.nf' +include { SPLICEAI2_PUBLISH } from '../../modules/nextflow/spliceai2/publish/main.nf' + +include { UCSC_WIGTOBIGWIG as WIGTOBIGWIG_DONOR_PLUS } from '../../modules/nextflow/ucsc/wigtobigwig/main.nf' +include { UCSC_WIGTOBIGWIG as WIGTOBIGWIG_DONOR_MINUS } from '../../modules/nextflow/ucsc/wigtobigwig/main.nf' +include { UCSC_WIGTOBIGWIG as WIGTOBIGWIG_ACCEPTOR_PLUS } from '../../modules/nextflow/ucsc/wigtobigwig/main.nf' +include { UCSC_WIGTOBIGWIG as WIGTOBIGWIG_ACCEPTOR_MINUS } from '../../modules/nextflow/ucsc/wigtobigwig/main.nf' + +include { BIGWIGMERGE as BIGWIGMERGE_DONOR_PLUS } from '../../modules/nextflow/bigtools/bigwigmerge/main.nf' +include { BIGWIGMERGE as BIGWIGMERGE_DONOR_MINUS } from '../../modules/nextflow/bigtools/bigwigmerge/main.nf' +include { BIGWIGMERGE as BIGWIGMERGE_ACCEPTOR_PLUS } from '../../modules/nextflow/bigtools/bigwigmerge/main.nf' +include { BIGWIGMERGE as BIGWIGMERGE_ACCEPTOR_MINUS } from '../../modules/nextflow/bigtools/bigwigmerge/main.nf' + +workflow SPLICEAI2 { + take: + genome // channel: [ val(meta), path(genome) ] + chromsizes // channel: [ path(chromsizes) ] + compression // bool + models // path(model directory) + species // SpliceAI2 species name + genome_fai // path(genome.fai), required with loci + loci // path(BED6), optional + ch_versions // channel: [ path(version) ] + + main: + ch_chunks = Channel.empty() + ch_transcripts = Channel.empty() + ch_skipped = Channel.empty() + SPLICEAI2_CHUNK(genome, compression) + + if (compression) { + SPLICEAI2_CHUNK.out.fasta_gz + .flatMap { + meta, fa -> + def fas = fa instanceof List ? fa : [fa] + fas.collect { it -> + def identifier = it.baseName.replaceFirst(/^tmp\./, '').replaceFirst(/\.fa$/, '') + [ [ id: meta.id + '.' + identifier ], it ] + } + } + .set { ch_chunks } + } else { + SPLICEAI2_CHUNK.out.fasta + .flatMap { + meta, fa -> + def fas = fa instanceof List ? fa : [fa] + fas.collect { it -> + def identifier = it.baseName.replaceFirst(/^tmp\./, '').replaceFirst(/\.fa$/, '') + [ [ id: meta.id + '.' + identifier ], it ] + } + } + .set { ch_chunks } + } + + SPLICEAI2_PREDICT(ch_chunks, models, species) + + WIGTOBIGWIG_DONOR_PLUS(SPLICEAI2_PREDICT.out.donor_plus.map { meta, wig -> [ [ id: meta.id + '.donor_plus' ], wig ] }, chromsizes) + WIGTOBIGWIG_DONOR_MINUS(SPLICEAI2_PREDICT.out.donor_minus.map { meta, wig -> [ [ id: meta.id + '.donor_minus' ], wig ] }, chromsizes) + WIGTOBIGWIG_ACCEPTOR_PLUS(SPLICEAI2_PREDICT.out.acceptor_plus.map { meta, wig -> [ [ id: meta.id + '.acceptor_plus' ], wig ] }, chromsizes) + WIGTOBIGWIG_ACCEPTOR_MINUS(SPLICEAI2_PREDICT.out.acceptor_minus.map { meta, wig -> [ [ id: meta.id + '.acceptor_minus' ], wig ] }, chromsizes) + + BIGWIGMERGE_DONOR_PLUS( + WIGTOBIGWIG_DONOR_PLUS.out.bigwig + .map { meta, bigwig -> bigwig } + .collect() + .map { bws -> [ [ id : 'donor_plus' ], bws ] }, + ) + BIGWIGMERGE_DONOR_MINUS( + WIGTOBIGWIG_DONOR_MINUS.out.bigwig + .map { meta, bigwig -> bigwig } + .collect() + .map { bws -> [ [ id : 'donor_minus' ], bws ] }, + ) + BIGWIGMERGE_ACCEPTOR_PLUS( + WIGTOBIGWIG_ACCEPTOR_PLUS.out.bigwig + .map { meta, bigwig -> bigwig } + .collect() + .map { bws -> [ [ id : 'acceptor_plus' ], bws ] }, + ) + BIGWIGMERGE_ACCEPTOR_MINUS( + WIGTOBIGWIG_ACCEPTOR_MINUS.out.bigwig + .map { meta, bigwig -> bigwig } + .collect() + .map { bws -> [ [ id : 'acceptor_minus' ], bws ] }, + ) + + SPLICEAI2_PUBLISH( + BIGWIGMERGE_DONOR_PLUS.out.bigwig, + BIGWIGMERGE_DONOR_MINUS.out.bigwig, + BIGWIGMERGE_ACCEPTOR_PLUS.out.bigwig, + BIGWIGMERGE_ACCEPTOR_MINUS.out.bigwig, + ) + + if (loci != null) { + SPLICEAI2_TRANSCRIPTS( + genome.map { meta, fasta -> [ meta, fasta, genome_fai, loci ] }, + models, + species + ) + ch_transcripts = SPLICEAI2_TRANSCRIPTS.out.transcripts + ch_skipped = SPLICEAI2_TRANSCRIPTS.out.skipped + } + + emit: + spliceai2 = SPLICEAI2_PUBLISH.out.spliceai2 + transcripts = ch_transcripts + skipped = ch_skipped + versions = ch_versions +} diff --git a/workflows/spliceai2/main.nf b/workflows/spliceai2/main.nf new file mode 100644 index 0000000..ddf70f7 --- /dev/null +++ b/workflows/spliceai2/main.nf @@ -0,0 +1,77 @@ +/* +Copyright (c) 2026 The Hiller Lab at the Senckenberg Gessellschaft für Naturforschung +Distributed under the terms of the Apache License, Version 2.0. +*/ + +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + IMPORT LOCAL SUBWORKFLOWS +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +*/ + +include { SPLICEAI2 as RUN } from '../../subworkflows/spliceai2/main.nf' +include { GENOME } from '../../subworkflows/genome/main.nf' + +params.genome = null +params.compression = false +params.spliceai2_models = null +params.spliceai2_species = 'GRCh38' +params.spliceai2_loci = null + +workflow SPLICEAI2 { + main: + if (!params.genome) { + error "Missing required --genome" + } + if (!params.spliceai2_models) { + error "Missing required --spliceai2_models" + } + + def genome_fasta = file(params.genome, checkIfExists: true) + def models = file(params.spliceai2_models, checkIfExists: true) + if (!models.toFile().isDirectory()) { + error "--spliceai2_models must be a directory" + } + + def compression = params.compression?.toString()?.toBoolean() ?: false + def species = (params.spliceai2_species ?: 'GRCh38').toString() + def genome_fai = null + def loci = null + if (params.spliceai2_loci) { + def uncompressed_genome = genome_fasta.toString().replaceFirst(/\.gz$/, '') + def fasta_name = uncompressed_genome.toLowerCase() + if (!(fasta_name.endsWith('.fa') || fasta_name.endsWith('.fasta'))) { + error "--spliceai2_loci requires a .fa or .fasta genome, optionally compressed as .gz" + } + genome_fai = file("${uncompressed_genome}.fai", checkIfExists: false) + if (!genome_fai.toFile().isFile()) { + error "Missing FASTA index '${genome_fai}' required by --spliceai2_loci; index the uncompressed FASTA (e.g. gunzip -k genome.fa.gz && samtools faidx genome.fa) and keep the .fai next to --genome" + } + loci = file(params.spliceai2_loci, checkIfExists: true) + if (!loci.toFile().isFile()) { + error "--spliceai2_loci must be a BED file" + } + } + + GENOME(genome_fasta) + + RUN( + GENOME.out.genome.map { g -> [ [ id: g.baseName ], g ] }, + GENOME.out.chrom_sizes, + compression, + models, + species, + genome_fai, + loci, + GENOME.out.versions + ) + + emit: + spliceai2 = RUN.out.spliceai2 + transcripts = RUN.out.transcripts + skipped = RUN.out.skipped +} + +workflow { + SPLICEAI2() +}