Pipelines: Nextflow and SLURM#
Two ways to run migec over more than one sample. They answer different questions: Nextflow when the pipeline continues past the consensus into alignment, calling or AIRR; SLURM when you want three commands over a cohort and nothing else.
Nextflow#
integrations/nextflow/ is an nf-core-style local module set with a runnable entry point.
main.nf --mode consensus | ctdna | airr
modules/migec/checkout/main.nf reads -> tagged FASTQ + QC tables
modules/migec/refine/main.nf tagged -> corrected FASTQ + barcode table + cells
modules/migec/assemble/main.nf corrected -> one consensus per molecule
modules/migec/plot/main.nf the tables -> SVG figures (needs gnuplot)
modules/downstream/align/main.nf consensus -> BAM, tags carried and checked
modules/downstream/callvariants/main.nf BAM -> VCF (LoFreq or Mutect2)
modules/downstream/arda/main.nf consensus -> AIRR clonotypes
subworkflows/migec/main.nf the three migec stages chained
subworkflows/migec_ctdna/main.nf + align + call: rare somatic variants
subworkflows/migec_airr/main.nf + arda: immune repertoires
nextflow.config defaults, every one read with getOrDefault
nextflow run integrations/nextflow -profile docker \
--mode ctdna --input 'data/*_R{1,2}.fq.gz' --preset tso500 \
--fasta ref.fa --outdir results/
nextflow run integrations/nextflow --mode airr --input 'data/*_R{1,2}.fq.gz' --preset migec
Or include a subworkflow in a pipeline of your own:
include { MIGEC } from './integrations/nextflow/subworkflows/migec/main'
workflow {
ch_reads = Channel.fromFilePairs(params.input)
.map { id, files -> [ [ id: id, preset: '10x-v2', payload_mate: 2 ], files ] }
MIGEC(ch_reads, file(params.cell_whitelist ?: 'NO_FILE'))
MIGEC.out.consensus.view()
}
Per-sample keys in meta win over the params.* defaults, so one run can mix chemistries:
bc_pattern, preset, read_structure, read_structure2, barcodes, max_offset,
payload_mate, expect_cells, rt_error, contig, fast, aligner, caller,
species.
What the downstream modules encode#
Two rules, both from Variant calling: which caller, and what it can possibly see:
Collapse first, then align once. Aligning raw reads and grouping on (position, UMI) is the other order in use, and it costs one alignment per read rather than per molecule, with the aligner seeing uncorrected sequence.
A standard variant caller, never a UMI-aware one. After assemble a caller’s depth already
is a molecule count. UMI-VarCal and UMIErrorCorrect group and consense themselves, so they
replace assemble rather than following it.
Never: do not set a family-size filter downstream of assemble. Every family has size 1 by
construction, so --min-family-size 3 discards the entire library and reports zero variants
without an error.
The align module checks its own output: if no MI:Z: tag reaches the BAM it exits non-zero and
names the flag each aligner needs (-y for minimap2 and minibwa map, -C for
bwa mem). Without that check the failure is silent and surfaces much later as an untagged BAM.
SLURM#
integrations/slurm/ is two sbatch templates and a sample sheet, for the case where the
deliverable is the consensus and a workflow engine would be ceremony.
# one sample
sbatch --export=ALL,R1=s1_R1.fq.gz,R2=s1_R2.fq.gz,SAMPLE=s1,PRESET=10x-v2,PAYLOAD_MATE=2 \
integrations/slurm/migec_sample.sbatch
# a cohort, one array task per row
sbatch --array=1-$(($(wc -l < samples.tsv) - 1)) \
integrations/slurm/migec_array.sbatch samples.tsv
Both run without SLURM as ordinary bash scripts – every SLURM variable has a fallback – which is how they are tested and how a layout should be checked before a cohort is queued:
R1=s1.fq.gz SAMPLE=s1 BC_PATTERN='0:12' bash integrations/slurm/migec_sample.sbatch
Note: array task 1 is the first data row. The header is skipped rather than counted, so the
range is 1-(rows - 1). An --array=0-N runs a task that reads the header as a sample and
fails somewhere confusing instead of at the sheet.
Sizing the request: checkout scales with reads (give it cores), refine with distinct
barcodes (give it memory; table_bytes in its JSON sizes the next run), assemble with one
bucket at a time (cores; peak memory is set by the bucket count, not the library). 16 cores and
32 GB covers a typical targeted or single-cell library.
Never: -t changes the wall clock and nothing else. Every stage is byte-identical at any thread
count, so a retry on a different node or an escalating --requeue cannot produce a result that
disagrees with the first attempt. That is what makes automatic retries safe here, and
tests/benchmark/ asserts it.
Why three processes#
The stages have different shapes, and one process forces one answer for all three:
stage |
scales with |
threads |
memory |
|---|---|---|---|
|
reads |
1,548,835 reads/s at 16 |
chunk-bounded, plus the UMI counters |
|
distinct barcodes |
1,554,156 reads/s at 16 |
the barcode table, ~96 B each |
|
reads, then buckets |
2,470,928 reads/s at 16 |
one bucket per worker |
Splitting them means a failed assemble resumes without re-running the demultiplex, and each
stage gets the label and the retry that fits it. refine carries process_high_memory because
its memory is set by the number of distinct barcodes and by nothing else: a 200 GB shallow run and
a 200 GB deep one need wildly different amounts of it, and FASTQ size predicts neither.
Retries are safe. Every stage’s output is byte-identical at any thread count
(Speed and memory), so an attempt that gets 4 cpus and an attempt that gets 32 produce the same
bytes – which is what makes errorStrategy 'retry' with escalating resources sound here rather
than merely convenient.
process {
withLabel: process_high { cpus = 16; memory = 32.GB }
withLabel: process_medium { cpus = 8; memory = 16.GB }
withLabel: process_high_memory { memory = { 64.GB * task.attempt } }
}
The three things that go wrong#
The barcode read is not always the payload read. On 10x, R1 is 26 nt of cell barcode and UMI
and nothing else. checkout is given both mates; the later stages must then run on the mate that
carries cDNA. payload_mate: 2 says so. Assuming R1 produces empty consensuses on every droplet
chemistry, and nothing in the run reports it as an error.
A per-sample `false` is not an absent value. Groovy’s ?: treats false as absent, so
[ id: 's1', contig: false ] against params.migec_contig = true would silently mean its
opposite. The modules use meta.containsKey('contig') ? meta.contig : params... for every
boolean, which is the one direction a per-sample override exists to make possible.
``–rt-error`` names a chemistry, not a number. rt (1e-4, caps at Q40) for anything with a
reverse transcription step, medium (1e-5) for an ordinary polymerase and no RT, high
(1e-6) for a proofreading one. It is the one-molecule floor – 10x’s Q60 requires two UMIs to
agree, and combining molecules is arda’s job, downstream
of this. See The RT/PCR floor — the quality no consensus can beat.
Smoke tests#
Every process has a stub: block, so nextflow run -stub-run walks the whole graph without
migec installed – the test to run before the real one.
params.migec_limit_read and params.migec_limit_umi pass through to the stages that take
them (--limit-read on all three, --limit-umi on refine and assemble), which gets an
answer out of a 400 GB run in a minute.
Warning
A limit is not a sample, and every limited run says so in its own report. Use subsample – a smaller library that is still a library when the output has to behave like the library.
Note
The -stub-run and the real run have not been executed on the machine these docs were
written on – nextflow is not installed there. The modules are reviewed against the nf-core
module spec, not verified by a pipeline run; treat the first run in your own pipeline as the
verification.
The SLURM templates are the opposite case: they have been run end to end here, as ordinary bash, producing a consensus and its figures, and their error paths were exercised too. Between the two, the migec commands themselves are covered either way – both call the same three.