peakatail reannotate¶
peakatail reannotate branches a completed peakatail run into a new trim / filter /
clustering variant without re-running peak calling. Peak calling (streaming
the BAMs) is the expensive stage; the trim (find_close) and everything
downstream of it (annotate → preprocess → cluster) is cheap and depends only on
artifacts a base run already wrote to disk. So a parameter sweep over
gene-distance, cell/PAS filters, or clustering settings can reuse one set of
raw peak calls and branch it many times.
It reuses the exact same internals as peakatail run's downstream section
(find_close + run_one_dataset_downstream + provenance reconcile + manifest
writing), so a branch is behaviourally identical to having run peakatail run with
those parameters — it just skips peak calling.
When to use it
- You have a finished
peakatail runand want to test different--max-gene-distance,--min-cells,--min-pas-per-cell,--resolution,--n-neighbors, or--cluster-methodvalues without paying for peak calling again. - You are running a sensitivity sweep (OFAT) over filter/clustering parameters from a single base peak-call.
When NOT to use it
- You want to change a peak-calling parameter (strategy, lambda,
prominence, merge). Those change the raw peaks, so you must re-run
peakatail run. - The base run has no
unified/concatenated.mtx+posbed.bed/negbed.bed(i.e. it wasn't produced by a recentpeakatail run).
The output is a complete, chainable run¶
--out is written as a full run directory — indistinguishable from a base
run's downstream output — so the new matrix continues straight into the next
steps:
- per-dataset
07_clustering/<ds>/clusters.h5ad(the new matrix + clustering) - E3 provenance:
provenance/by_dataset/{pas,cell}_ledger.tsv+reconcile_summary.json - E2
run_manifest.json(hub-indexable) 03_gtf_annotation/<ds>/annotatedpas.bed, rootpasbed.bed,branch_manifest.json
So peakatail switch diff, peakatail switch length, peakatail switch trend, and the hub all
consume a reannotated run exactly as they would a base run.
One branch, one --out (exit code 1 if not)¶
Every branch must have its own --out directory. Two peakatail reannotate
processes pointed at the same --out write the same
04_pas_gene_assignment/<ds>/pas_gene.tsv, 05_annotated_matrix/<ds>/* and
07_clustering/<ds>/clusters.h5ad paths, so the artifacts that survive are an
arbitrary interleaving of two different parameter sets. This is not
hypothetical: five branches of a sweep grid that shared a branch_name (hence
an --out) ran concurrently under Nextflow and produced three different
pas_gene.tsv row counts for identical declared parameters, grouped by
write time.
So peakatail reannotate now refuses to start when another live peakatail reannotate
already holds the same --out. It takes an exclusive flock on
<out>/.ema_reannotate.lock before any work begins; if that claim fails it
exits immediately with exit code 1, writing nothing, and prints who holds
the directory:
$ peakatail reannotate --base-run runs/base --out runs/branch_gd3000 --gtf ref.gtf
Error: --out is already in use by another running `peakatail reannotate`:
/path/runs/branch_gd3000 (held by pid=48211 host=node07 started=2026-09-03T23:34:14)
— two branches writing one output dir interleave their artifacts and silently
corrupt both. Give every branch its OWN --out; a duplicate branch_name in a
sweep grid is the usual cause. (Lock file: /path/runs/branch_gd3000/.ema_reannotate.lock)
$ echo $?
1
What to do when you hit this
- Sweep grids are the usual cause. Two rows resolving to the same
branch_name— and therefore the same${out_root}/reannotate/<branch_name>— is a duplicate--out, not two branches. De-duplicatebranch_name(or fold the differing parameters into it) so every row gets a distinct directory, then re-run. - Genuinely want both variants? Give the second one a different
--out. Branches are cheap; they share the base run's peak calls. - Want to overwrite an earlier branch? Wait for the holder named in the
message to finish (or stop it), then re-run into the same
--out. - Not a stale lock. The claim is held by the running process's open
file description, so the kernel releases it the moment that process exits
— including a crash or a
kill. A branch that died never blocks the re-run, and you never need to delete.ema_reannotate.lockby hand. If the refusal fires, somepeakatail reannotatereally is alive on that directory.
The lock is advisory and only guards against a second peakatail reannotate; it
does not stop an unrelated program from writing into --out.
.ema_reannotate.lock is a zero-value bookkeeping file (dot-prefixed, never
listed in run_manifest.json/branch_manifest.json) and is safe to leave in
place.
Artifacts are written atomically¶
Text artifacts — including pas_gene.tsv, annotatedpas.bed, pasbed.bed,
the annotated-matrix pas_ids/barcodes sidecars, run_config.json,
run_manifest.json and the per-stage *_stats.json — are written to a temp
file in the same directory and then os.replace()d onto the final path.
A reader therefore never observes a partially written or truncated artifact:
at any instant the final path holds either the complete previous content or
the complete new content, and a failed write leaves the previous file
untouched. Nothing about the file contents changes — only when they become
visible.
Quick example¶
# Branch a base run into a 3000 bp gene-distance + resolution-0.5 variant
uv run peakatail reannotate \
--base-run peakatail_runs/emaout_2026-05-11_120000 \
--out peakatail_runs/branch_gd3000_res0p5 \
--gtf Homo_sapiens.GRCh38.99.gtf \
--max-gene-distance 3000 \
--resolution 0.5 \
--threads 8
Flags¶
Every trim / filter / clustering knob run_one_dataset_downstream accepts is a
CLI flag — nothing is pinned internally.
Inputs¶
| Flag | Type | Default | Meaning |
|---|---|---|---|
--base-run |
DIR | — (required) | A completed peakatail run output dir to branch from. |
--out |
DIR | — (required) | Where to write the new variant run. Must be unique per branch — a second peakatail reannotate on the same --out is refused with exit code 1, see One branch, one --out. |
--gtf |
FILE | — (required) | Same GTF as the base run. |
Trim (find_close)¶
| Flag | Type | Default | Meaning |
|---|---|---|---|
--max-gene-distance |
INT | 5000 | Max distance (bp) for gene-end annotation. |
--utr-multiplier |
FLOAT | 2.0 | 3'UTR extension multiplier. |
--include-extended |
flag | off | Keep PAS in the extended (TIER_3) region. |
Cell / PAS filters¶
| Flag | Type | Default | Meaning |
|---|---|---|---|
--min-read |
INT | 1500 | Minimum reads per cell barcode. |
--min-cells |
INT | 3 | Minimum cells expressing a PAS. |
--min-pas-per-cell |
INT | 50 | Minimum PAS per cell. |
Clustering¶
| Flag | Type | Default | Meaning |
|---|---|---|---|
--cluster-method |
TEXT | leiden_tfidf | leiden_tfidf, leiden_libsize, or external. |
--resolution |
FLOAT | 1.0 | Leiden resolution. |
--n-neighbors |
INT | strategy default | kNN graph neighbours (30 tfidf / 10 libsize). |
--n-pcs |
INT | 40 | Principal components. |
--n-svd-components |
INT | 50 | SVD components (LSI). |
--n-top-hvg |
INT | 2000 | Top HVGs (leiden_libsize only). |
--tfidf-scale-factor |
FLOAT | 10000 | TF-IDF scale factor (leiden_tfidf only). |
--depth-corr-threshold |
FLOAT | 0.75 | Depth-correlation drop threshold (leiden_tfidf only). |
--external-clusters |
PATH | — | Cluster labels TSV (--cluster-method external). |
--random-seed |
INT | 42 | Clustering RNG seed. |
Plus the shared --threads, --config, and logging flags documented on the
CLI index.