Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions modules/nf-core/sativaepang/looplace/environment.yml
Original file line number Diff line number Diff line change
@@ -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::sativa-epang=0.9.3.4
69 changes: 69 additions & 0 deletions modules/nf-core/sativaepang/looplace/main.nf
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
process SATIVAEPANG_LOOPLACE {
tag "$meta.id"
label 'process_high'

conda "${moduleDir}/environment.yml"
container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ?
'https://depot.galaxyproject.org/singularity/sativa-epang:0.9.3.4--py314hab16a5f_0' :
'quay.io/biocontainers/sativa-epang:0.9.3.4--py314hab16a5f_0' }"

input:
tuple val(meta), path(taskdir)

output:
tuple val(meta), path(taskdir), emit: taskdir
tuple val("${task.process}"), val('sativaepang'), eval("sed -n 's#.*share/sativa-epang-\\([0-9.]*\\)-.*#\\1#p' \$(command -v sativa-epang)"), topic: versions, emit: versions_sativaepang
Comment thread
erikrikarddaniel marked this conversation as resolved.
Outdated

when:
task.ext.when == null || task.ext.when

script:
def args = task.ext.args ?: ''
"""
# \$taskdir is staged from sativaepang/lootasks's own output -- a symlink under
# local/shared-filesystem staging, but a real copy under stageInMode 'copy' (the
# default for cloud storage without Fusion). loo-place writes a jplace (and logs)
# inside every fold directory, which would mutate that other task's output in place
# and make this task uncacheable across -resume (same class of bug as
# nf-core/modules#12799). Give it a private, writable directory tree instead: fold
# subdirectories are real (new) directories here, their read-only contents
# (ref.nwk/ref.fasta/query.fasta) stay symlinks to avoid copying large alignments,
# and manifest.json at the top level is a plain symlink since loo-place never writes
# there. mv handles both staging modes: it moves a symlink as a symlink, and renames
# a real directory in place, so nothing is deleted either way.
mv "$taskdir" "${taskdir}.staged"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Surely just stage the original with this name instead.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same reasoning as in my reply above — happy to take another look if you still see room to trim further once you've seen the shrunk version.

real_taskdir=\$(readlink -f "${taskdir}.staged")
mkdir "$taskdir"
find "\$real_taskdir" -mindepth 1 -maxdepth 1 | while read -r entry; do
name=\$(basename "\$entry")
if [ -d "\$entry" ]; then
mkdir "$taskdir/\$name"
find "\$entry" -mindepth 1 -maxdepth 1 -exec ln -s {} "$taskdir/\$name"/ \\;
else
ln -s "\$entry" "$taskdir/\$name"
fi
done

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a lot of code, is it really required?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some form of private writable staging is required: sativa-epang writes results into every fold directory, and taskdir can be staged as a plain symlink straight into lootasks' own task directory (default local-executor staging) — writing into it in place would mutate that other task's output and break -resume the same way #12799 did.

You're right that the generic version was overkill though: it was walking an arbitrary file/directory tree, but lootasks' output schema is actually fixed (manifest.json + fold_*/{ref.nwk,ref.fasta,query.fasta}, confirmed against a real multi-fold run — nothing else ever appears). Pushed in adf8590: hardcoded to that shape instead, about a third of the size.


sativa-epang \\
-stage loo-place \\
-taskdir $taskdir \\
-T ${task.cpus} \\
${args}
"""

stub:
"""
mv "$taskdir" "${taskdir}.staged"
real_taskdir=\$(readlink -f "${taskdir}.staged")
mkdir "$taskdir"
find "\$real_taskdir" -mindepth 1 -maxdepth 1 | while read -r entry; do
name=\$(basename "\$entry")
if [ -d "\$entry" ]; then
mkdir "$taskdir/\$name"
touch "$taskdir/\$name/epa_result.jplace"
else
ln -s "\$entry" "$taskdir/\$name"
fi
done
"""
}
68 changes: 68 additions & 0 deletions modules/nf-core/sativaepang/looplace/meta.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
name: "sativaepang_looplace"
description: Place every leave-one-out fold with EPA-ng via -stage loo-place, writing a jplace into each fold directory
keywords:
- phylogenetics
- taxonomy
- mislabelling
- placement
- leave-one-out
tools:
- "sativaepang":
description: "SATIVA with EPA-ng as the placement engine"
homepage: "https://github.com/Aaramis/sativa-epang"
documentation: "https://github.com/Aaramis/sativa-epang/blob/main/CHANGES-epa-ng.md"
tool_dev_url: "https://github.com/Aaramis/sativa-epang"
doi: "10.1093/nar/gkw396"
licence:
- "GPL-3.0-or-later"
identifier: ""
input:
- - meta:
type: map
description: Groovy Map containing sample information. e.g. `[ id:'sample1' ]`
- taskdir:
type: directory
description: |
The `*.l1o_tasks` directory produced by `sativaepang/lootasks`: one
subdirectory per fold (`ref.nwk`, `ref.fasta`, `query.fasta`) plus a
`manifest.json`.
pattern: "*.l1o_tasks"
output:
taskdir:
- - meta:
type: map
description: Groovy Map containing sample information. e.g. `[ id:'sample1' ]`
- taskdir:
type: directory
description: |
The same taskdir, with an `epa_result.jplace` (plus `epang.log` and
`epa_info.log`) added to every fold subdirectory. Input to
`sativaepang/looscore`.
pattern: "*.l1o_tasks"
versions_sativaepang:
- - ${task.process}:
type: string
description: The name of the process
- sativaepang:
type: string
description: The name of the tool
- sed -n 's#.*share/sativa-epang-\([0-9.]*\)-.*#\1#p' \$(command -v sativa-epang):
type: eval
description: The expression to obtain the version of the tool

topics:
versions:
- - ${task.process}:
type: string
description: The name of the process
- sativaepang:
type: string
description: The name of the tool
- sed -n 's#.*share/sativa-epang-\([0-9.]*\)-.*#\1#p' \$(command -v sativa-epang):
type: eval
description: The expression to obtain the version of the tool

authors:
- "@erikrikarddaniel"
maintainers:
- "@erikrikarddaniel"
134 changes: 134 additions & 0 deletions modules/nf-core/sativaepang/looplace/tests/main.nf.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
nextflow_process {

name "Test Process SATIVAEPANG_LOOPLACE"
script "../main.nf"
process "SATIVAEPANG_LOOPLACE"

tag "modules"
tag "modules_nfcore"
tag "sativaepang"
tag "sativaepang/lootasks"
tag "sativaepang/looplace"

test("sativa-epang tiny example") {

setup {
run("SATIVAEPANG_LOOTASKS") {
script "../../lootasks/main.nf"
process {
"""
input[0] = [
[ id:'test' ],
file(params.modules_testdata_base_path + 'delete_me/sativaepang/sativaepang_tiny.refjson', checkIfExists: true),
file(params.modules_testdata_base_path + 'delete_me/sativaepang/sativaepang_tiny.model', checkIfExists: true)
]
"""
}
}
}

when {
process {
"""
input[0] = SATIVAEPANG_LOOTASKS.out.taskdir
"""
}
}

then {
// Every fold in the returned taskdir must have a jplace, not just process.success.
// (Separately verified by hand, not expressible cleanly here: the upstream
// SATIVAEPANG_LOOTASKS task's own directory on disk is left untouched -- this
// module stages a private, writable copy rather than mutating the upstream
// task's output in place, the same class of -resume-breaking bug fixed in
// nf-core/modules#12799.)
// EPA-ng's placement likelihoods aren't byte-reproducible between the conda
// environment and the container builds (confirmed: identical logs/inputs,
// differing epa_result.jplace content). epa_info.log/epang.log also embed a
// wall-clock "Elapsed Time: Ns" line that flips between runs under load, on any
// engine. Mask all three in the snapshot -- same precedent as
// sigprofiler/snapaligner's unstablePatterns usage -- and rely on the structural
// asserts below for the guarantee this module actually makes.
def taskdir = file(process.out.taskdir[0][1])
def foldDirs = taskdir.listFiles().findAll { it.isDirectory() && it.name.startsWith("fold_") }
assertAll(
{ assert process.success },
{ assert foldDirs.size() > 0 },
{ assert foldDirs.every { new File(it.toString(), "epa_result.jplace").exists() } },
{ assert snapshot(sanitizeOutput(process.out, unstablePatterns: ["**/epa_result.jplace", "**/epa_info.log", "**/epang.log"])).match() }
)
}
}

test("gtdb archaea 16S") {

setup {
run("SATIVAEPANG_LOOTASKS") {
script "../../lootasks/main.nf"
process {
"""
input[0] = [
[ id:'test' ],
file(params.modules_testdata_base_path + 'delete_me/sativaepang/gtdb_archaea_16s.refjson', checkIfExists: true),
file(params.modules_testdata_base_path + 'delete_me/sativaepang/gtdb_archaea_16s.model', checkIfExists: true)
]
"""
}
}
}

when {
process {
"""
input[0] = SATIVAEPANG_LOOTASKS.out.taskdir
"""
}
}

then {
def taskdir = file(process.out.taskdir[0][1])
def foldDirs = taskdir.listFiles().findAll { it.isDirectory() && it.name.startsWith("fold_") }
assertAll(
{ assert process.success },
{ assert foldDirs.size() > 0 },
{ assert foldDirs.every { new File(it.toString(), "epa_result.jplace").exists() } },
{ assert snapshot(sanitizeOutput(process.out, unstablePatterns: ["**/epa_result.jplace", "**/epa_info.log", "**/epang.log"])).match() }
)
}
}

test("sativa-epang tiny example - stub") {

options "-stub"

setup {
run("SATIVAEPANG_LOOTASKS") {
script "../../lootasks/main.nf"
process {
"""
input[0] = [
[ id:'test' ],
file(params.modules_testdata_base_path + 'delete_me/sativaepang/sativaepang_tiny.refjson', checkIfExists: true),
file(params.modules_testdata_base_path + 'delete_me/sativaepang/sativaepang_tiny.model', checkIfExists: true)
]
"""
}
}
}

when {
process {
"""
input[0] = SATIVAEPANG_LOOTASKS.out.taskdir
"""
}
}

then {
assertAll(
{ assert process.success },
{ assert snapshot(process.out).match() }
)
}
}
}
Loading