Walkthrough: from POD5 to modification calls¶
This is the path for someone who has just finished a sequencing run. It starts from nothing on the cluster: clone the pipeline, install it, bring the run over with rsync, describe the samples, update the config, and run. By the end you will have aligned modBAMs, per-site modification tables and QC for every sample. Each step says what to do, what to check, and what can go wrong.
The example throughout is a gDNA run of two samples, wt_rep1 and
mut_rep1, aligned to CHM13v2.0 with 5mC/5hmC and 6mA called, on Bodhi.
Adapt the names and paths; the Alpine differences are called out where they
matter.
Already basecalled by MinKNOW?
If MinKNOW basecalled the run live with the models you want, you can skip
dorado entirely: transfer bam_pass/ along with the POD5 and set
basecalled: true in step 6. Everything
else is identical.
0. What you need¶
- An account on Bodhi or Alpine with access to a GPU partition (or a workstation with an NVIDIA GPU for small runs).
- The run directory MinKNOW wrote, on the sequencer or a machine you can reach with ssh.
- A reference genome FASTA (plain or bgzipped).
- For a barcoded run, the barcoding kit name (e.g.
SQK-NBD114-24) and which barcode is which sample.
1. Clone the pipeline and install the environment¶
Pick a home for code. On Bodhi anywhere under your home directory is fine; on
Alpine use /projects/$USER, never $HOME (2 GB).
mkdir -p ~/devel && cd ~/devel
git clone https://github.com/rnabioco/nanopore-dna-modification-pipeline.git
cd nanopore-dna-modification-pipeline
pixi install
export PIXI_CACHE_DIR=/scratch/alpine/$USER/.cache/pixi # before the first install
mkdir -p /projects/$USER/devel && cd /projects/$USER/devel
git clone https://github.com/rnabioco/nanopore-dna-modification-pipeline.git
cd nanopore-dna-modification-pipeline
pixi install
pixi install builds .pixi/envs/default from the committed lock file:
snakemake, the Slurm executor, samtools, minimap2, modkit, mosdepth and the
Python libraries. A few minutes, about 1.4 GB. If pixi is not on your PATH
yet, Installation covers installing it.
2. Install dorado, the models and escpod¶
pixi run setup
Once, from one node. It downloads dorado 2.1.2 (3.4 GB, checksum verified),
the models the base config names (sup@v5.2.0 plus its 5mC_5hmC and 6mA
models) and escpod, all under resources/ in the checkout. Check:
pixi run list-models
Every line should say present. DNAscent is optional and separate
(pixi run install-dnascent, 5 GB); see DNAscent.
3. Transfer the run with rsync¶
Decide where raw data lives. It is read by every job and is the one thing the pipeline never copies, so put it somewhere stable:
| Cluster | Raw data | Notes |
|---|---|---|
| Bodhi | /beevol/home/$USER/data/nanopore/<run> (or a shared /beevol/data/... location) |
one shared filesystem; check sinteractive quota --check first, a flowcell is 100–700 GB |
| Alpine | /scratch/alpine/$USER/data/<run> |
fast, not backed up, purged after ~90 days untouched; archive to PetaLibrary (/pl/active/<allocation>) when done |
Then copy the run. Copying takes hours for a PromethION flowcell, so run it as a job rather than in your login shell, and use flags that let it resume:
DEST=/beevol/home/$USER/data/nanopore/20260901_gDNA
mkdir -p "$DEST"
srun -p rna -c 2 --mem 4G -t 12:00:00 -J rsync-run --comment=rsync-run -- \
rsync -avP --partial-dir=.rsync-partial \
<user>@<sequencer-host>:/data/20260901_gDNA/ "$DEST/"
The compute node needs ssh access to the source (keys in place). On
Alpine use the data-transfer partition instead of a general one:
srun -p dtn --qos=dtn ....
rsync -avP --partial-dir=.rsync-partial \
/data/20260901_gDNA/ \
<user>@<cluster-login>:/beevol/home/<user>/data/nanopore/20260901_gDNA/
For very large runs on Alpine, Globus is the supported route; see the CURC data-transfer documentation.
What the flags do, and what to watch:
-akeeps timestamps and permissions,-vlists files,-Pis--partial --progress: an interrupted copy resumes where it stopped when you run the same command again.--partial-dirkeeps half-copied files out of the way until they are complete.- Trailing slashes matter.
source/copies the contents ofsourceintoDEST/;source(no slash) createsDEST/source/. - Do not add
-z: POD5 is already compressed and compressing it again only costs CPU. - Add
--exclude 'fastq_*'if MinKNOW also wrote FASTQ; the pipeline does not use it. Keeppod5_pass,pod5_fail,bam_pass(if basecalled live) and the*_summary_*files.
Verify before going further. Run the same rsync once more with -n
(dry run); it should list nothing. Then compare counts and sizes on both
sides, and open one file:
find "$DEST" -name '*.pod5' | wc -l # same number as on the sequencer?
du -sh "$DEST"
pixi run escpod summary "$DEST"/pod5_pass/*_0.pod5
escpod summary prints the flow cell, kit, sample rate and read count. For a
final byte-level check on an important run, rsync -avc (checksums) once.
This is the last copy
Everything from here on references $DEST. The pipeline hard-links or
symlinks the POD5 into its output tree; it never duplicates the run.
4. Look at the run directory¶
MinKNOW writes a run like this:
/beevol/home/me/data/nanopore/20260901_gDNA/
├── pod5_pass/ <- what the pipeline uses
│ ├── PAX12345_pass_0.pod5
│ └── ...
├── pod5_fail/ <- also used: dorado re-decides pass/fail
├── bam_pass/ <- only if MinKNOW basecalled live
├── sequencing_summary_*.txt
└── final_summary_*.txt
A barcoded run has one subdirectory per barcode instead:
pod5_pass/barcode01/, pod5_pass/barcode02/, ... and bam_pass/barcode01/
if it was basecalled live.
The pipeline needs R10.4.1, 5 kHz data (kit SQK-LSK114 or a barcoding kit
of that generation) for the default dna_r10.4.1_e8.2_400bps models; the
escpod summary line above tells you. A 4 kHz file (2022 and earlier) needs
a v4.x model instead.
Decide which of the three shapes each sample is:
| You have | Mode | What the pipeline does |
|---|---|---|
| POD5 for one sample, one or more run directories | basecall |
dorado basecaller per sample |
| One pooled run, barcoded, not basecalled with the models you want | demux |
one dorado pass with --kit-name, then dorado demux |
MinKNOW bam_pass already basecalled with the models you want |
prebasecalled |
BAMs used as-is; POD5 only needed for DNAscent |
5. Pick the reference¶
Any FASTA works. Put it somewhere shared and stable; the pipeline links it
into the output tree and builds its own indexes there (.fai, minimap2 .mmi,
chrom sizes), so the source directory can be read-only.
ls -la /beevol/data/ref/chm13v2.0.fa
If different samples need different references (a spike-in, another organism), the samples file can say so per sample.
6. Write the samples file¶
config/gdna-samples.tsv, no header, whitespace-separated. A sample id
repeated on several lines has several run directories (they are
basecalled together).
wt_rep1 /beevol/home/me/data/nanopore/20260901_gDNA_wt
wt_rep1 /beevol/home/me/data/nanopore/20260908_gDNA_wt_topup
mut_rep1 /beevol/home/me/data/nanopore/20260901_gDNA_mut
A third column overrides the reference for that sample.
config/gdna-samples.yml. kit is what dorado uses to classify barcodes.
runs:
- path: /beevol/home/me/data/nanopore/20260901_pooled
kit: SQK-NBD114-24
samples:
wt_rep1: barcode01
mut_rep1: barcode02
basecalled: true means dorado is not run; MinKNOW's
bam_pass/<barcode>/*.bam are concatenated per sample. Whatever models
MinKNOW ran are what you get (the @RG line in the BAM says which).
runs:
- path: /beevol/home/me/data/nanopore/20260901_pooled
basecalled: true
samples:
wt_rep1: barcode01
mut_rep1: barcode02
Sample ids become directory and file names: letters, digits, ., _, -;
no spaces. The full format, including per-sample references and mixing
barcoded and unbarcoded runs, is in Samples file.
7. Update the config¶
Create config/gdna.yml. It is layered over config/config-base.yml, so it
only needs the keys that differ; every other key keeps the documented default.
samples: config/gdna-samples.tsv
output_directory: /beevol/home/me/results/gdna-2026-09
reference:
fasta: /beevol/data/ref/chm13v2.0.fa
models:
simplex: dna_r10.4.1_e8.2_400bps_sup@v5.2.0
modified_bases: [5mC_5hmC, 6mA]
modkit:
cpg: true # CpG-context 5mC table with strands combined;
combine_strands: true # drop both for all-context C and A calls
Choices worth a moment:
- Which modifications.
5mC_5hmC(all-context C),5mCG_5hmCG(CpG only, a little more accurate there),4mC_5mC,6mA. At most one model per canonical base:5mC_5hmC+6mAstack,5mC_5hmC+4mC_5mCdo not. Short codes expand to pinned versions; see Models. - Whole-genome or CpG pileup.
modkit.cpg: truerestricts the bedMethyl to CpG sites and needs a C model. Leave it off for 6mA-only work or all-context analyses. - Output directory. A new directory per project. On Alpine it must be on
/scratch/alpine.
If you changed the models, resolve and install them:
pixi run list-models --configfile config/gdna.yml
pixi run install-models --configfile config/gdna.yml # only if something is MISSING
simplex present dna_r10.4.1_e8.2_400bps_sup@v5.2.0
ont_mod present dna_r10.4.1_e8.2_400bps_sup@v5.2.0_5mC_5hmC@v2 (canonical C)
ont_mod present dna_r10.4.1_e8.2_400bps_sup@v5.2.0_6mA@v1 (canonical A)
8. Dry run¶
Always. It costs seconds and catches most config mistakes before any GPU time is spent:
pixi run snakemake -n --configfile config/gdna.yml
Read the top of the output and the job table at the bottom:
Samples: wt_rep1 [basecall], mut_rep1 [basecall]
Model stack: dna_r10.4.1_e8.2_400bps_sup@v5.2.0 + ..._5mC_5hmC@v2 + ..._6mA@v1
...
Job stats:
job count
------------------- -------
link_sample_pod5 2
stage_reference 1
basecall 2
mm2_index 1
align 2
finalize_bam 2
samtools_qc 2
mosdepth 2
dorado_read_summary 2
modkit_pileup 2
modkit_summary 2
samples_summary 1
all 1
total 22
Things the dry run tells you:
samples file error: no POD5 files found ...: the run path is wrong, or a barcoded run was given without a barcode. Fix the samples file.model stack error: ... both call canonical base C: two C models are listed; drop one.model not installed: ...(a warning): runinstall-models.dorado 2.1.2 is not installed(a warning): runpixi run setup.- The
Samples:line shows the mode each sample resolved to. If a sample you expected to bedemuxshowsbasecall, the samples file gave it no barcode.
9. Launch on the cluster¶
The Snakemake controller submits one Slurm job per rule and waits. Run it
from a login shell or an sinteractive session, inside something that
survives you logging out (tmux, screen, or the sinteractive session
itself). Do not run it inside a GPU allocation.
scontrol show reservation # a maintenance window? see below
pixi run run-bodhi --configfile config/gdna.yml
Basecalling goes to the gpu partition (account gpu_rbi, 2 A30s per
job), everything else to rna. Details: Bodhi.
pixi run run-alpine --configfile config/gdna.yml
Basecalling goes to aa100 with QOS gpu-normal (24 h cap; the profile
comments say how to move to gpu-long), everything else to acpu.
Details: Alpine.
pixi run run --configfile config/gdna.yml --cores 16
Maintenance reservations
A job that asks for more walltime than remains before a maintenance
reservation is silently deferred until after it. scontrol show
reservation shows what is scheduled; the profile's runtime for
basecall is 36 h on Bodhi.
How long? dorado sup basecalling with two modification models runs at very roughly 1.5 to 2 million reads per hour per pair of A30s. A 10 Gb sample is an hour or two; a full PromethION flowcell is most of a day. If the basecall job is killed at its walltime, just relaunch: the partial output is kept and dorado resumes from it.
10. Monitor¶
squeue --me -o "%.10i %.28j %.10P %.10M %.8T %R" # the jobs Snakemake submitted
tail -f /beevol/home/me/results/gdna-2026-09/logs/basecall/wt_rep1.log
Job names carry the rule and sample. Logs live in two places:
| Where | What |
|---|---|
<output_directory>/logs/<rule>/<sample>.log |
the tool's own stderr (dorado progress, modkit log) |
logs/slurm/ in the pipeline directory |
Slurm's stdout/stderr per job; kept for failed jobs |
If a job fails, Snakemake keeps going with everything that does not depend on
it (keep-going: true) and exits non-zero at the end. Read the log, fix the
cause, and run the same command again: finished outputs are not redone.
11. What you get¶
/beevol/home/me/results/gdna-2026-09/
├── bam/final/wt_rep1/wt_rep1.bam(.bai) aligned, sorted; MM/ML modbase tags and mv move table kept
├── summary/
│ ├── modkit/wt_rep1/
│ │ ├── wt_rep1.pileup.bed.gz(.tbi) bedMethyl: one row per site, strand and mod code
│ │ └── wt_rep1.modkit_summary.tsv fraction modified per mod code, sampled
│ ├── qc/wt_rep1/ flagstat.tsv, stats.txt, mosdepth.summary.txt
│ ├── tables/wt_rep1/wt_rep1.read_summary.tsv.gz per read: length, qscore, alignment
│ └── samples_summary.tsv one row per sample
├── reference/chm13v2.0-<hash>/ genome.fa (link), .fai, .mmi, chrom.sizes
├── pod5/samples/wt_rep1/pod5/ links to the input POD5
├── bam/basecall/wt_rep1/wt_rep1.bam dorado's unaligned output (large; see cleanup)
├── logs/
└── manifest.json commit, config, resolved models, tool versions
First things to look at:
column -t summary/samples_summary.tsv
zcat summary/modkit/wt_rep1/wt_rep1.pileup.bed.gz | head -3
A bedMethyl row is chrom start end mod_code valid_coverage strand ... N_valid
fraction_modified N_mod N_canonical ...; m is 5mC, h 5hmC, a 6mA. The
Outputs page lists every file and column.
To view in IGV, load bam/final/<sample>.bam (IGV colours modified bases from
the MM/ML tags) or set modkit.bigwig: true for one bigWig of modification
fraction per mod code.
12. Common next steps¶
- Compare two samples: add a
dmr.contrastsentry ({name: mut_vs_wt, a: mut_rep1, b: wt_rep1, base: C}) and rerun; you getsummary/dmr/mut_vs_wt.dmr.bedfrommodkit dmr pair. - Per-read calls:
modkit.extract_calls: true(large tables). - Thresholds:
modkit.filter_threshold/modkit.mod_thresholds, or restrict to motifs withmodkit.motifs: ["GATC,1"]. - BrdU/EdU and replication forks:
dnascent.enabled: true; see DNAscent. - Your own analysis on every final BAM: a
custom:entry; see Custom steps. - Reclaim space once you are done with the raw basecalls:
pixi run clean --configfile config/gdna.ymlremoves the unaligned BAMs and POD5 links and keepsbam/finalandsummary/. Setcleanup_intermediates: truein the config to do this automatically during the run. - Archive the raw run (Alpine especially): move
$DESTto PetaLibrary before the scratch purge, and rerun from there if needed.
Troubleshooting¶
| Symptom | Likely cause | Fix |
|---|---|---|
rsync stops partway, or Permission denied (publickey) from a job |
no ssh key from the compute node to the source host | push from the source instead, or set up a key; rerun the same rsync to resume |
no POD5 files found for sample |
wrong path, or a barcoded run without a barcode | check ls <run>/pod5_pass; give the barcode in a YAML samples file |
dorado job fails immediately with a CUDA error |
the job did not get a GPU, or ran on a CPU partition | check the profile's basecall entry; nvidia-smi in the job log |
| basecall killed at walltime | run larger than the profile's runtime |
relaunch (resumes), or raise runtime for basecall |
modkit pileup finds no modification calls |
the BAM has no MM/ML tags: MinKNOW basecalled without mod models | set basecalled: false to re-basecall, or accept no mod tables |
Everything ran but samples_summary.tsv shows 0 mapped reads |
wrong reference, or 4 kHz data with a 5 kHz model | escpod summary on a POD5; check summary/qc/<s>/<s>.flagstat.tsv |
| Controller "hangs" after the first batch on Bodhi | sacct unreachable from the node |
the profile already uses slurm-status-command: squeue; make sure you use it |