Part of the MuMDIA developer documentation (see docs/README.md).
MuMDIA has deterministic native implementations for its default predictor and
rescorer paths, so it runs with zero Python. The sidecars are the opt-in path to real
predictors and rescorers that raise identification counts over the native
defaults. Each sidecar is a standalone Python worker invoked as a subprocess over
a positional-CLI file contract (the sidecar file contract; see
docs/18_findings_and_decisions.md): the Rust caller writes an input file
(Parquet or PIN), runs python <worker> <argv...>, and reads an output Parquet
keyed by id or candidate_id. There is no JSON request file, no long-lived
server, and no stdin/stdout data channel; the files on disk are the entire
interface. Any sidecar can be swapped for a native Rust implementation (or a
different tool) without touching the callers. Failure handling is not uniform:
only the rescore sidecars fall back to a native rescorer, and only when
rescore.strict = false. The predictor sidecars and MBR have no fallback and no
strict gate, so a crashed MS2PIP, DeepLC, or DeepLC fine-tune aborts the whole
run. The matrix in Failure behavior below is authoritative.
The 11 scripts split into four groups: predictors (ms2pip_worker,
deeplc_worker, deeplc_finetune) feed the run-independent library;
rescorers (mokapot_worker, nn_rescore_worker, entrapment_worker) score
the competed PSMs in Stage F; MBR (mbr_worker) transfers identifications
across runs (Stage D3); and the DIA-NN recipe (import_diann_lib,
make_reverse_decoys, make_shift_decoys, augment_library) is an offline,
one-time toolchain that builds MuMDIA's target+decoy library schema. Three of the
four convert a user-produced DIA-NN library into that schema; augment_library
is the fourth, filling an imported library with the tryptic FASTA peptides it is
missing (a completeness fix) before decoy generation.
Two groups, verified in the source. Only the rescore sidecars fall back; the
predictor sidecars and MBR abort the run on any nonzero exit. run_worker turns a
nonzero exit into an Err (sidecar.rs:229-231); the call site decides whether
that Err aborts or is caught.
| worker (stage) | on nonzero exit | strict gate | fallback |
|---|---|---|---|
| ms2pip_worker (predict-frag) | aborts run | none | none: run_ms2pip(...)? propagates (predict_frag.rs:333-341); an empty result map is also a hard bail! (predict_frag.rs:342-344) |
| deeplc_worker (predict-frag) | aborts run | none | none: run_deeplc(...)? propagates (predict_frag.rs:291) |
| deeplc_finetune (run) | aborts run | none | none: run_deeplc_finetune(...)? propagates (run.rs:253-263) |
mbr_worker (mumdia mbr) |
aborts command | none | none: run_mbr(...)? propagates (main.rs:697-710) |
| mokapot_worker (rescore) | falls back / bails | rescore.strict |
native_tda when strict=false, else bail (rescore.rs:172-180) |
| nn_rescore_worker (rescore) | falls back / bails | rescore.strict |
native_tda when strict=false, else bail (rescore.rs:199-207) |
| percolator (rescore, unwired) | falls back / bails | rescore.strict |
native_tda when strict=false, else bail (rescore.rs:209-215) |
| entrapment_worker (rescore) | falls back / bails | rescore.strict |
native linear entrapment rescorer (still QMode::Entrapment) when strict=false, else bail (rescore.rs:255-264) |
A crashed MS2PIP, DeepLC, DeepLC fine-tune, or MBR worker aborts. A crashed
rescorer aborts only under rescore.strict = true; otherwise its scores are
silently replaced by the native path.
The positional arguments each Rust caller passes, the output file it reads, and the column it keys the readback on.
| worker | positional args in | output file | key column |
|---|---|---|---|
| ms2pip_worker | <in.parquet> <out.parquet> <model> (sidecar.rs:63) |
ms2pip_out.parquet |
id |
| deeplc_worker | <in.parquet> <out.parquet> (sidecar.rs:99) |
deeplc_out.parquet |
id |
| deeplc_finetune | <lib_in> <seed> <lib_out> --epochs --patience --q-train --batch (sidecar.rs:139-151) |
<lib_out> (= fragment_library_precursors_ft.parquet) |
peptidoform (new table with replaced predicted_irt; input unchanged) |
| mokapot_worker / nn_rescore_worker | <rescore.pin> <out.parquet> + env MUMDIA_NN_FOLDS/ITERS/TRAIN_FDR (rescore.rs:781-792) |
rescore_sidecar_out.parquet |
candidate_id (echoes the flat row index) |
| entrapment_worker | <in.parquet> <out.parquet> <folds> (rescore.rs:718-724) |
entrapment_out.parquet |
row_id |
| mbr_worker | <scored> <psms_csv> <out> --q-anchor --min-anchor-runs --q-transfer --seed [--out-scored] [--frag-csv --consensus-corr-min] (sidecar.rs:193-211) |
<out>.parquet |
candidate_id |
For the mapping from each conda environment to the config field that points at it
(predict_frag.ms2pip_python, predict_frag.deeplc_python, rescore.python,
mbr.python) and the interpreter paths on this machine, see
docs/19_getting_started.md.
| path | role |
|---|---|
scripts/ms2pip_worker.py |
Predictor: MS2PIP b/y fragment intensities per peptidoform+charge |
scripts/deeplc_worker.py |
Predictor: DeepLC iRT per peptidoform (uncalibrated) |
scripts/deeplc_finetune.py |
Predictor: transfer-learn DeepLC on this run's seed, rewrite library iRT |
scripts/mokapot_worker.py |
Rescorer: mokapot brew over a PIN (model env-switchable: nn/logreg/xgb/percolator) |
scripts/nn_rescore_worker.py |
Rescorer: PyTorch semi-supervised MLP over a PIN, in-memory or streaming memmap |
scripts/entrapment_worker.py |
Rescorer: GBM/NN on real-target-vs-spike-in negatives, out-of-fold by base peptide |
scripts/mbr_worker.py |
MBR (Stage D3): cross-run RT transfer + permuted-RT decoy-transfer FDR |
scripts/import_diann_lib.py |
Recipe: DIA-NN fragment-level parquet -> MuMDIA target lib_precursors+lib_fragments |
scripts/make_reverse_decoys.py |
Recipe: reverse-sequence decoys with no-target-overlap invariant |
scripts/make_shift_decoys.py |
Recipe: fragment-shift (CH2) decoys, DIA-NN-style terminal shift |
scripts/augment_library.py |
Recipe: augment an imported library with its missing tryptic FASTA peptides, then hand off to a decoy builder |
rust/mumdia/crates/mumdia/src/sidecar.rs |
Rust clients: resolve_script, run_ms2pip, run_deeplc, run_deeplc_finetune, run_mbr, run_worker |
rust/mumdia/crates/mumdia/src/stages/predict_frag.rs |
Call sites for MS2PIP + DeepLC (Stage C) |
rust/mumdia/crates/mumdia/src/stages/run.rs |
Call site for DeepLC fine-tune (between search-seed and rt-im-train) |
rust/mumdia/crates/mumdia/src/stages/rescore.rs |
Call sites for mokapot/nn_torch (PIN) + entrapment (Parquet) sidecars |
rust/mumdia/crates/mumdia/src/main.rs |
Call site for MBR (Cmd::Mbr) + doctor env probe |
env/docker-rescore.yml |
Docker env rescore: mokapot 0.10.0 + ms2pip 4.0.0.dev9 (py3.11) |
env/docker-deeplc.yml |
Docker env deeplc: DeepLC 4.0 multitask (pinned commit) + CPU torch (py3.11) |
env/mumdia-rescore.yml |
Minimal portable env for the mokapot logreg rescore path (py3.12) |
Each sidecar's on-disk contract, with the exact column schema read/written by the code.
ms2pip_worker (sidecar.rs:42 run_ms2pip)
- IN
ms2pip_in.parquet:idu32,peptidoformstr (ProForma),chargei32. - OUT
ms2pip_out.parquet:idu32,ion_typestr ("b"/"y"),ordinali32 (1-based),intensityf32 (linear). Rust folds this intoHashMap<u32, HashMap<(u8 ion_byte, u16 ordinal), f32>>(sidecar.rs:70-76).
deeplc_worker (sidecar.rs:81 run_deeplc)
- IN
deeplc_in.parquet:idu32,peptidoformstr. - OUT
deeplc_out.parquet:idu32,predicted_rtf32. Rust returnsHashMap<u32, f32>(sidecar.rs:104).
deeplc_finetune (sidecar.rs:111 run_deeplc_finetune)
- IN
<lib_in>=fragment_library_precursors.parquet(needspeptidoform,predicted_irt);<seed>= seed PSMs (peptidoform,label,spectrum_q,observed_rt). - OUT
<lib_out>=fragment_library_precursors_ft.parquet: the input table with thepredicted_irtcolumn replaced (deeplc_finetune.py:156-159). Same schema, values rewritten.
mokapot_worker / nn_rescore_worker (rescore.rs:740 run_pin_sidecar)
- IN
rescore.pin: Percolator tab-separated. Fixed columnsSpecId Label ScanNr ExpMass CalcMass <features...> Peptide Proteins(rescore.rs:760-777).SpecId = psm_<i>,ScanNr = <i>whereiis the unique flat row index (NOTcandidate_id, which repeats across runs);ExpMass=CalcMass=precursor_mz({:.5}); features are{:.6};Label+1 target / -1 decoy;Peptide = -.<peptidoform>.-(Percolator flanking dots);Proteins = <protein>(single column,rescore.rs:776).nn_rescore_worker.pybuilds its fold key from thePeptidecolumn, stripping the mod brackets and theX./.Xflanks (strip_pep,nn_rescore_worker.py:70-74) before themd5 % FOLDShash. The feature column order is taken from the first competed input'sFeatureSchemacompanion (rescore.rs:74), so all competed inputs must share one schema. - OUT
rescore_sidecar_out.parquet:candidate_idu32 (echoes the SpecId tail = row index),scoref64,q_valuef64 (written as zeros; Rust computes q). Rust mapsscoreback by row index and validates exact/unique/finite coverage (align_sidecar_scores,rescore.rs:802-805).
entrapment_worker (rescore.rs:675 run_entrapment_gbm)
- IN
entrapment_in.parquet:row_idu32 (unique flat index),candidate_idu32,base_peptide_idu32,is_entrapmenti32 (0/1),is_decoyi32 (0/1), then one f64 column per feature (rescore.rs:693-714). - OUT
entrapment_out.parquet:row_idu32,candidate_idu32,scoref64. Rust maps back byrow_id(rescore.rs:729-732).
mbr_worker (sidecar.rs:162 run_mbr)
- IN
<scored_combined>: experiment-wide scored table, columns read =candidate_id, source, label, q_value, peptidoform, charge, protein_group(mbr_worker.py:81-82).<psms_csv>: comma-joined per-run psms.parquet paths insourceorder, each read forcandidate_id, apex_rt(mbr_worker.py:96). Optional--frag-csv: per-run fragment_quant paths (candidate_id, fragment_name, quantity). - OUT
<out>.parquet: one row per accepted transfer withcandidate_id, source, peptidoform, charge, protein_group, label, expected_rt, observed_rt, rt_delta, transfer_q(mbr_worker.py:254-265). Optional--out-scoredwrites the scored table with accepted transfers'q_valuelowered totransfer_qand anis_transferredflag added. Optional--emit-transfer-targetswrites per-runrun_windows-format tables (candidate_id, rt_pred_cal, rt_lo, rt_hi, im_*) plus a permuted-RT decoy file for the re-extraction tier (mbr_worker.py:142-151).
import_diann_lib (offline; no Rust caller)
- IN
<diann_lib.parquet>: DIA-NN fragment-level speclib (columnsDecoy, Fragment.Loss.Type, Fragment.Type, Modified.Sequence, Precursor.Charge, Precursor.Mz, RT, Stripped.Sequence, Product.Mz, Relative.Intensity, Fragment.Series.Number, Fragment.Charge, Protein.Names/Protein.Ids). - OUT
<out_precursors>:candidate_id, peptidoform_id, base_peptide_id, peptidoform, charge, precursor_mz, predicted_irt, label(="target"), protein, n_fragments.<out_fragments>:candidate_id, mz, predicted_intensity, name, ion_type, ordinal, frag_charge(import_diann_lib.py:59-83).
make_reverse_decoys / make_shift_decoys (offline; no Rust caller)
- IN/OUT the same precursor+fragment schema as
import_diann_lib, reading a target-only (or target-half) library and emitting a target+decoy library re-sorted byprecursor_mzwith contiguous re-indexedcandidate_id.
augment_library (offline; no Rust caller, but shells out to the mumdia
binary and to a decoy builder)
- IN
--fasta(protein FASTA),--imported-precursors/--imported-fragments(the imported target+decoy library in theimport_diann_libschema), plus--mumdia-binand--work-dir(all required). The imported library supplies the set of TARGET base sequences already present (rows withlabel == "target",augment_library.py:100-114); the FASTA plusmumdia digest/peptidoformssupplies the candidate set to complete against. - OUT
--out-precursors/--out-fragments: a full target+decoy library in the sameimport_diann_libschema, containing the imported targets plus the missing tryptic peptides plus paired decoys. No schema/artifact-version bump: the output is the same fragment_library precursor/fragment column layout, only re-indexed. - FLAGS:
--config(default None),--match-level {base_sequence(default), peptidoform_charge},--decoy-strategy {shift(default),reverse}(augment_library.py:67-91).
MS2PIP (predict_frag.rs:324-393, worker ms2pip_worker.py). Selected by
predict_frag.predictor = "ms2pip". assign_intensities collects one id per
Raw candidate, its peptidoform and charge (the id is the flat raws index,
not candidate_id, predict_frag.rs:330), calls run_ms2pip, then for each
fragment looks up (ion_byte, ordinal) for charge-1 fragments only; charge-2
fragments fall back to the native heuristic (predict_frag.rs:356-363). Because
MS2PIP charge-1 (TIC-fraction, ~0.02-0.3) and the native charge-2 fallback
(max-normalized, ~0.19-0.5) live on different scales, ranking them together in
top-N would bury MS2PIP, so each charge group is max-normalized to its own peak
before they compete (predict_frag.rs:365-384). Two native-fallback edge cases:
run_ms2pip returning an empty map is a hard error (bail!("MS2PIP returned no predictions"), predict_frag.rs:342-344), while a single candidate that MS2PIP
returned nothing for (missing/empty per-id entry) falls back wholesale to the
native intensities for that candidate (predict_frag.rs:387-389). The
worker builds psm_utils.PSMList in 100k-row chunks, calls
ms2pip.predict_batch(model, processes=min(8, cpu_count)), and converts MS2PIP's
log2 intensities to linear via 2**x - 0.001 clipped at 0
(ms2pip_worker.py:52-53). Ordinals are emitted 1-based
(ms2pip_worker.py:57). The __main__ guard makes the Windows spawn start
method safe for multiprocessing.
DeepLC predict (predict_frag.rs:274-312, worker deeplc_worker.py).
Selected by predict_frag.rt_predictor = "deeplc". assign_rt deduplicates by
peptidoform (RT is charge-independent, predict_frag.rs:281-290), calls
run_deeplc, and writes r.irt. Peptidoforms with no returned iRT are anchored
at 0.0 with a warning (predict_frag.rs:293-308; this is the "unmatched
peptidoforms silently get iRT 0.0" foot-gun noted in CLAUDE.md). The worker calls
deeplc.predict in 200k chunks and, when the multitask model returns an
ensemble matrix (N, n_models), averages across models (deeplc_worker.py:34-35).
Predictions are uncalibrated; rt-im-train's per-run LOESS/linear maps them onto
observed RT.
DeepLC fine-tune (run.rs:242-263, worker deeplc_finetune.py). Wired into
run only when rt_im_train.finetune_deeplc = true; runs between
search-seed and rt-im-train, rewriting predicted_irt in a copy of the library
(fragment_library_precursors_ft.parquet) that rt-im-train and extract then read.
Algorithm: (1) build the reference from confident target seed PSMs with
spectrum_q <= q_train and a standard-AA sequence (deeplc_finetune.py:99-104);
(2) auto-scale batch size so each epoch runs >= ~30 gradient steps, clamped to
[16, 512] (deeplc_finetune.py:114-117) because a fixed 512 underfits a small
(~4k) E.coli seed; (3) deeplc.finetune(ref_psms, train_kwargs) transfer-learns
the weights (deeplc_finetune.py:128); (4) predict every unique standard
peptidoform on its DECOY_-stripped underlying sequence so decoys land on the
same iRT scale as targets (deeplc_finetune.py:44-45, 135-156). A peptidoform
that is non-standard (is_std false, e.g. a terminal mod outside STD) or was
not predicted keeps its original predicted_irt unchanged, because the
write-back is preds.get(base_pf(pf), orig[i]) (deeplc_finetune.py:156), so
only the sequences DeepLC actually re-predicted move onto the fine-tuned scale.
Beyond the four flags Rust passes (--epochs/--patience/--q-train/--batch,
sidecar.rs:139-151), the worker exposes CLI-only knobs that run never sets:
--device cpu|cuda (cuda aborts with SystemExit if torch.cuda.is_available()
is false, deeplc_finetune.py:79-81), --threads (torch CPU pool, defaults to
DEEPLC_FT_THREADS), --max-ref N (cap reference PSMs), --predict-limit N
(cap peptidoforms predicted), and --skip-predict (fine-tune only, exercise the
crash path without the full-library prediction, deeplc_finetune.py:71-76, 131-133). The long
docstring and the thread-cap block at the top exist to prevent an intermittent
machine crash: numpy's OpenBLAS (GNU OpenMP) and torch's Intel OpenMP coexist
under KMP_DUPLICATE_LIB_OK=TRUE, and without pinning OMP/MKL/OPENBLAS to 1
thread and bounding torch's pool, the two full thread pools oversubscribe the CPU
during the backward pass (deeplc_finetune.py:6-28, 82-91). --device cuda
sidesteps this entirely by moving compute off the CPU pools.
mokapot and nn_torch share the exact PIN contract via run_pin_sidecar
(rescore.rs:740-806). Rust concatenates the competed feature tables, writes the
PIN, spawns the worker, and passes MUMDIA_NN_FOLDS/ITERS/TRAIN_FDR from
rescore.{folds,num_iter,train_fdr} as env vars (rescore.rs:790-792) so the
report's recorded params match what ran; mokapot ignores those three. On success
the classifier label and model_identity are recorded; on failure the path falls
back to native_scores only when rescore.strict = false; strict is the
production default. The authoritative actual path is recorded in
psms_scored.parquet.report.json.
mokapot_worker.pyreads the PIN withmokapot.read_pin, builds a model chosen byMUMDIA_RESCORE_MODEL(make_model,mokapot_worker.py:35-98:nn-> sklearnMLPClassifier;logreg->LogisticRegression;xgb->XGBClassifier;percolator/linear/svm-> mokapot's default,model=None), runsmokapot.brew(..., rng=0, max_workers=MUMDIA_MOKAPOT_WORKERS), and uses mokapot's out-of-fold confidence scores only (each PSM scored by the fold that did not train on it,mokapot_worker.py:128-163). There is deliberately no in-sample fallback:_oof_scoresraisesRuntimeErrorunless the merged target+decoy confidence tables cover the PIN rows exactly once with finite scores (mokapot_worker.py:152-162), matching the CLAUDE.md rule that fold-model averaging is not an acceptable fallback. ThatRuntimeErroris a nonzero worker exit, which the Rust caller then treats perrescore.strict(bail if strict, elsenative_tda). Every PSM (targets and decoys) is scored,SpecIdtail parsed tocandidate_id(mokapot_worker.py:171).nn_rescore_worker.pyimplements the semi-supervised scheme itself in PyTorch. Fold assignment ismd5(stripped_peptide) % FOLDS(nn_rescore_worker.py:127), so peptides never leak across folds and the split is deterministic. Per fold it selects the initial feature+sign using a deterministic sample of training rows only, then iterates {recompute target-decoy q on the training folds -> targets atq<=TRAIN_FDRpositive, all decoys negative -> train MLP from scratch -> rescore} forITERSrounds, then scores the held-out fold. Empty, single-class, and zero-positive training folds hard-error; held-out labels do not influence model selection. Two feature backends behind one accessorget: in-memory (median/IQR standardisation,:130-145) or a disk-backed float32 memmap streamed inMUMDIA_NN_CHUNKchunks with mean/std accumulated in one text pass (:146-177), auto-selected when the PIN exceedsMUMDIA_NN_STREAM_GB(4 GB). This is what makes an experiment-wide multi-run rescore tractable: the full PIN never lives in RAM.tda_q(:77-87) is the shared q formula(decoys+1)/max(1,targets), running min from the tail. Seeds are ensembled by averaging rank-normalised OOF scores (:281-288).
entrapment (rescore.rs:675-733, worker entrapment_worker.py). Selected by
rescore.classifier = "entrapment" with rescore.entrapment_marker set and
rescore.python present; otherwise it falls back to a native linear entrapment
rescorer or native_tda (rescore.rs:217-276). classify_entrapment
(rescore.rs:577-604) marks a target as entrapment when its protein contains the
marker, does not contain entrapment_exclude, and matches none of
entrapment_contaminant_markers. The worker trains real-target (positive) vs
spike-in (negative), decoys excluded from training (entrapment_worker.py:80-83),
out-of-fold with GroupKFold grouped by base_peptide_id
(entrapment_worker.py:93-101); a final model fit on all non-decoy PSMs scores
decoys and any single-class-fold gaps (:103-108). Model is gbm
(HistGradientBoostingClassifier, early_stopping=False so random_state=0 is
reproducible) or nn (StandardScaler + MLP pipeline) via
MUMDIA_ENTRAPMENT_MODEL (entrapment_worker.py:28-60). The rationale: spike-in
negatives experience the same chimeric DIA interference as real targets, so a
flexible model helps (AUC ~0.97 vs ~0.62 on in-silico decoys), unlike the
decoy-trained regime, where a native linear model is all in-silico decoys can
support. Selecting entrapment (whether the GBM
sidecar or the native linear fallback) flips the internal QMode from Decoy to
Entrapment (rescore.rs:145, 252/262/273), so every q level (PSM, per-run,
peptide, protein-group, precursor) is computed by entrapment_q against the
real-target-vs-spike-in null scaled by rescore.entrapment_ratio, and the
reported IDs are the real targets only (spike-in excluded, rescore.rs:400-403).
The report also records entrapment_peptides_at_1pct, a leak check on spike-in
peptides passing the 1% gate (rescore.rs:437-445, 492).
mbr_worker.py (main.rs:671-711 Cmd::Mbr, sidecar.rs:162 run_mbr).
Requires mbr.strategy != none, >= 2 runs, and mbr.python
(main.rs:680-692). Reads the experiment-wide scored table and per-run apex RTs.
It builds per-run to-reference / from-reference RT maps by monotone binned-median
calibration against run 0 (binned_map, mbr_worker.py:31-43, needs >= 200
shared anchors else identity). For a precursor confident (target, q<=q_anchor)
in >= min_anchor_runs OTHER runs but sub-threshold in a target run where it was
still extracted (the rescuable tier), it predicts RT as the from-ref map of
the median of the other runs' to-ref-mapped apex RTs (expected_rt,
mbr_worker.py:116-121). The false-transfer FDR uses a permuted-RT
decoy-transfer null: each candidate is assigned a shuffled candidate's predicted
RT, and transfer q is standard target/decoy competition on |observed - predicted|
(mbr_worker.py:190-199). An optional fragment-consensus cosine guard
(--frag-csv/--consensus-corr-min) rejects RT-concordant interference
(mbr_worker.py:209-241). --emit-transfer-targets instead emits per-run
run_windows for the absent set (confident elsewhere, not extracted here) so
extract --restrict-candidates --run-windows can re-extract them (the
re-extraction tier). run_mbr (sidecar.rs:162-213) drives only the rescuable
tier: it passes <scored> <psms_csv> <out>, the --q-anchor/--min-anchor-runs/ --q-transfer/--seed values, and optionally --out-scored and (only when frag
paths are given and consensus_corr_min > 0) --frag-csv/--consensus-corr-min.
It never passes --emit-transfer-targets or --rt-window, so the re-extraction
tier is a manual worker invocation. --seed receives the engine-wide
rng_seed (main.rs:709), not an MBR-specific field. Note the MBR sidecar is
validated as a prototype but Stage D3 is a stub in the engine (config hooks only;
not in the run chain).
Run once by the user, who must hold their own DIA-NN license (MuMDIA ships no
DIA-NN). import_diann_lib.py filters to targets, b/y no-loss fragments, and
peptides carrying only Carbamidomethyl/Oxidation (the only mapped mods,
import_diann_lib.py:28-37), rewrites (UniMod:4/35) to ProForma bracket names,
sorts precursors by m/z with a stable mergesort and assigns contiguous
candidate_id (:52-56), derives base_peptide_id by factorizing
Stripped.Sequence (:55), and builds each fragment name as
<Fragment.Type><Fragment.Series.Number> with a ^<z> suffix appended when
Fragment.Charge > 1 (:72-74), preserving species-flagged protein names for the
ProteoBench metric. Then either decoy builder adds the null population:
make_reverse_decoys.py reverses each target keeping the C-terminal residue,
recomputes the real b/y m/z of the reversed sequence from a 20-residue monoisotopic
mass table plus a UNIMOD mod-mass dict (Carbamidomethyl, Oxidation, Acetyl,
Phospho, Deamidated, Methyl, Dimethyl, Carbamyl; an unknown bracket name falls
back to parsing a numeric +mass string, make_reverse_decoys.py:26-50),
validated against the library's own target m/z to < 5 ppm at the 99th percentile
over the first 500 target precursors (make_reverse_decoys.py:97-107), and
enforces a hard no-overlap
invariant: any reversed stripped sequence colliding with a real target (palindrome
or reverse-equals-another-target) or with a decoy sequence already owned by a
different target base sequence is re-scrambled by a per-peptide-seeded
Fisher-Yates, dropped after MAX_TRIES=30, and a final assertion requires
decoy_stripped ∩ target_stripped == {} (:115-132, 166-169). make_shift_decoys.py is
the alternative: copy intensities+iRT, keep precursor m/z, and shift each fragment
in m/z space by -DELTA/z (b ions) or +DELTA/z (y ions) where
DELTA = 14.015650 Da (one CH2) and z = frag_charge, net precursor shift zero
(make_shift_decoys.py:17, 41-44). Its decoy peptidoform and protein are the
target strings prefixed with DECOY_ (make_shift_decoys.py:35-36); the reverse
builder instead sets peptidoform = "DECOY_" + <reversed-sequence ProForma> and
protein = "DECOY_" + <target protein> (make_reverse_decoys.py:145-146). Both
concatenate
target+decoy, re-sort by precursor_mz, reassign contiguous candidate_id, and
re-sort fragments by candidate_id so the fragment index's contiguous-range
precondition holds (make_reverse_decoys.py:156-161, make_shift_decoys.py:47-55).
augment_library.py closes a different gap: an imported DIA-NN library can be
missing tryptic peptides that are present in the FASTA, so the search DB
structurally cannot find them. It fixes this completeness gap by reusing the
engine's own stages, which guarantees the augmented peptidoform strings are
byte-identical to what the native path would emit
(augment_library.py:1-37). The flow is: (1) run mumdia digest (with
N-terminal Met-excision on) then peptidoforms over the FASTA
(augment_library.py:96-98); (2) set-diff the resulting base sequences against
the imported library's TARGET base (stripped) sequences, keeping only the missing
ones (augment_library.py:103-104, 110-114); (3) run mumdia predict-frag on the
missing set to produce native predicted spectra and iRT
(augment_library.py:126-131); (4) offset peptidoform_id/base_peptide_id/
candidate_id so the new entries are disjoint from the imported ids
(augment_library.py:137-144); (5) per-precursor (groupby candidate_id)
max-normalize predicted_intensity (augment_library.py:148-149); (6) merge the
imported targets with the missing targets (augment_library.py:156-162); (7) hand
the merged target library to make_shift_decoys.py (default) or
make_reverse_decoys.py for the paired, collision-free decoy population
(augment_library.py:166-171); (8) validate load invariants on the final library:
candidate_id contiguous over 0..N-1, precursor_mz monotonically increasing,
and both target and decoy labels present (augment_library.py:178-193). The
pairing and collision-free guarantee lives in the downstream decoy builder, not in
augment_library.py itself; its own validate step only checks contiguity,
precursor_mz ordering, both-labels presence, and (for --decoy-strategy reverse
only) target/decoy stripped-sequence non-overlap
(augment_library.py:190-193). The predicted entries' RT axis need not match the
imported DIA-NN iRT axis, because the per-run DeepLC fine-tune
(rt_im_train.finetune_deeplc) re-predicts iRT for the whole library, putting
every entry on one axis before extraction, so no explicit reconciliation is done
(augment_library.py:21-23). CLI contract (all positional-style flags): required
--fasta --imported-precursors --imported-fragments --out-precursors --out-fragments --mumdia-bin --work-dir, optional --config (default None),
--match-level {base_sequence(default),peptidoform_charge}, and --decoy-strategy {shift(default),reverse} (augment_library.py:67-91).
| name | file:line | what it does |
|---|---|---|
resolve_script |
sidecar.rs:20 |
Resolve a worker path: CWD-relative dir, then <exe_dir>/<dir>, then <exe_dir>/scripts, else CWD-relative fallback |
run_worker |
sidecar.rs:217 |
Spawn python <script> <argv...>; utf8=true sets PYTHONUTF8/PYTHONIOENCODING (DeepLC/Keras crash on Windows cp1252) |
run_ms2pip |
sidecar.rs:42 |
Write ms2pip_in.parquet, run worker, fold output to HashMap<u32,HashMap<(u8,u16),f32>> |
run_deeplc |
sidecar.rs:81 |
Write deeplc_in.parquet, run worker, return HashMap<u32,f32> (utf8=true) |
run_deeplc_finetune |
sidecar.rs:111 |
Run deeplc_finetune.py <lib_in> <seed> <lib_out> --epochs --patience --q-train --batch (utf8=true) |
run_mbr |
sidecar.rs:162 |
Run mbr_worker.py <scored> <psms_csv> <out> [--out-scored] [--frag-csv --consensus-corr-min] --q-anchor --min-anchor-runs --q-transfer --seed |
run_pin_sidecar |
rescore.rs:740 |
Write PIN keyed by row index, run mokapot/nn worker, map score back by row index |
run_entrapment_gbm |
rescore.rs:675 |
Write features+meta Parquet, run entrapment worker, map score back by row_id |
align_sidecar_scores |
rescore.rs:811 |
Validate + align a sidecar's (row_id, score): exact, unique, finite coverage or bail |
make_model |
mokapot_worker.py:35 |
Build the mokapot model from MUMDIA_RESCORE_MODEL (nn/logreg/xgb/percolator) |
tda_q |
nn_rescore_worker.py:77 |
Target-decoy q: (decoys+1)/max(1,targets), running min from the tail |
one_pass |
nn_rescore_worker.py:225 |
One CV pass -> OOF scores; per fold iterate positive re-selection + MLP retrain |
binned_map |
mbr_worker.py:31 |
Monotone binned-median RT calibration x->y (80 bins) |
expected_rt |
mbr_worker.py:116 |
Cross-run predicted RT of a candidate in a run from the other runs' anchors |
frag_mz / reverse_keep_cterm / stable_seed / splitmix |
make_reverse_decoys.py:58/56/74/65 |
Residue-mass b/y m/z; C-term-fixed reversal; process-independent FNV-1a seed; seeded PRNG for scramble |
Config was recently pruned; every field below exists in mumdia-core/src/config.rs
today. Predictor/rescorer selection is by strategy enum, and sidecars are engaged
only when a non-native enum is set AND the corresponding Python interpreter is
configured.
| field | default | effect |
|---|---|---|
predict_frag.predictor |
native |
ms2pip engages ms2pip_worker.py (requires ms2pip_python) |
predict_frag.rt_predictor |
native |
deeplc engages deeplc_worker.py (requires deeplc_python) |
predict_frag.ms2pip_model |
"HCD" |
3rd positional arg to ms2pip_worker.py |
predict_frag.ms2pip_python |
None |
interpreter for MS2PIP (env with ms2pip+pyarrow) |
predict_frag.deeplc_python |
None |
interpreter for DeepLC predict AND fine-tune |
predict_frag.sidecar_script_dir |
"scripts" |
dir passed to resolve_script for all workers |
rt_im_train.finetune_deeplc |
false |
engage deeplc_finetune.py in run (requires deeplc_python) |
rt_im_train.finetune_epochs |
25 |
--epochs upper bound (early stopping usually halts first) |
rt_im_train.finetune_patience |
10 |
--patience epochs without val-loss improvement |
rt_im_train.finetune_batch |
0 |
--batch; 0 = auto-scale to seed size in the worker |
rt_im_train.q_train |
0.01 |
--q-train max seed spectrum_q for the fine-tune reference |
rescore.classifier |
native_tda |
mokapot/nn_torch -> PIN sidecar; entrapment -> Parquet sidecar; percolator -> unwired |
rescore.python |
None |
interpreter for the rescore/entrapment sidecar |
rescore.folds |
3 |
passed as MUMDIA_NN_FOLDS + entrapment folds argv |
rescore.num_iter |
10 |
passed as MUMDIA_NN_ITERS (native semi-supervised iterations) |
rescore.train_fdr |
0.01 |
passed as MUMDIA_NN_TRAIN_FDR |
rescore.strict |
true |
fail on any rescorer sidecar failure/misconfiguration; false explicitly enables compatibility fallback |
rescore.entrapment_marker |
None |
protein substring marking spike-in negatives (required for entrapment) |
rescore.entrapment_exclude |
None |
substring that un-marks the sample's own species |
rescore.entrapment_contaminant_markers |
[] |
substrings for genuine contaminants (kept as real targets) |
rescore.entrapment_ratio |
1.0 |
N_real_lib / N_entrap_lib scaling of the entrapment FDR |
mbr.strategy |
none |
empirical_library/rt_transfer/full engage mbr_worker.py |
mbr.python |
None |
interpreter for MBR (required when strategy != none) |
mbr.q_anchor / min_anchor_runs / q_transfer |
0.01 / 2 / 0.01 |
--q-anchor / --min-anchor-runs / --q-transfer (anchor/transfer FDR + min supporting runs) |
mbr.consensus_corr_min |
0.0 |
--consensus-corr-min fragment-consensus guard threshold (0 = off; only passed when frag paths are also supplied, sidecar.rs:209) |
mbr.rt_window_s |
20.0 |
transfer half-window (>= p95 M2 residual ~17 s) for the --emit-transfer-targets re-extraction tier. Not wired through run_mbr (sidecar.rs:193-211 passes neither --rt-window nor --emit-transfer-targets); the worker uses its own --rt-window default of 20.0 when run by hand. |
mbr.decoy_transfer |
permuted_rt |
DecoyTransfer enum (permuted_rt/reverse_sequence/both, config.rs:857-869) selecting the false-transfer null. Unwired: run_mbr never passes it and the worker implements only the permuted-RT null (mbr_worker.py:176-199). |
mbr.requant_all |
false |
requantify already-identified precursors, not only transfers; only meaningful for strategy = full. Unwired in the current mbr_worker.py. |
Worker-only env knobs (not config fields; set in the process environment).
mokapot_worker.py: MUMDIA_RESCORE_MODEL (default nn; accepts the aliases
logreg/logistic/lr, xgb/xgboost, and percolator/linear/svm,
mokapot_worker.py:36-76), MUMDIA_BREW_ITERS (20, the Model.max_iter
semi-supervised count), MUMDIA_NN_HIDDEN (128,64,64,32), MUMDIA_NN_SOLVER
(adam), MUMDIA_NN_MAX_ITER (200), MUMDIA_NN_ALPHA (1e-4); logreg reads
MUMDIA_LR_C (1.0) and MUMDIA_LR_MAX_ITER (1000); xgb reads MUMDIA_XGB_TREES
(200), MUMDIA_XGB_DEPTH (6), MUMDIA_XGB_LR (0.1), MUMDIA_XGB_JOBS (0 = all
cores); MUMDIA_MOKAPOT_WORKERS (3, thread-based CV-fold parallelism).
nn_rescore_worker.py: MUMDIA_NN_EPOCHS (25), MUMDIA_NN_HIDDEN (128,64),
MUMDIA_NN_DROPOUT (0.3), MUMDIA_NN_LR (1e-3), MUMDIA_NN_WD (1e-4),
MUMDIA_NN_BATCH (4096), MUMDIA_NN_SEEDS (1), MUMDIA_NN_STREAM (auto),
MUMDIA_NN_STREAM_GB (4), MUMDIA_NN_CHUNK (250000), MUMDIA_NN_INIT_SAMPLE
(300000) plus the three the Rust caller injects: MUMDIA_NN_FOLDS (worker default
3), MUMDIA_NN_ITERS (worker default 5, but run_pin_sidecar overrides it with
rescore.num_iter = 10), MUMDIA_NN_TRAIN_FDR (0.01). These worker defaults
apply only when the sidecar is run standalone. entrapment_worker.py:
MUMDIA_ENTRAPMENT_MODEL (gbm|nn). deeplc_finetune.py: DEEPLC_FT_THREADS
(8) plus argparse flags. Note the mokapot worker's code default model is nn
(the sklearn MLP inside mokapot), even though the recommended portable path
(env/mumdia-rescore.yml) sets MUMDIA_RESCORE_MODEL=logreg; the Rust caller
does not set MUMDIA_RESCORE_MODEL, so unless the environment sets it you get the
MLP. Set it explicitly for the logreg path.
- File contract only. No sidecar reads stdin or emits data on stdout (only log
lines). The output Parquet must key on
idorcandidate_id/row_idexactly as the caller expects, or the readback map silently assigns the worst score. - Row index vs candidate_id. In multi-run rescoring
candidate_idis the library index and repeats across runs, so the PIN keys on a unique flat row index (SpecId=psm_<i>,ScanNr=<i>,rescore.rs:763-771) and the entrapment Parquet carries a separaterow_id(rescore.rs:697). Keying oncandidate_idwould collide and collapse runs. - resolve_script Windows-path gotcha. The build target dir is redirected off
the OneDrive tree (
C:/Users/robbi/mumdia_build/...), whilescripts/lives under the OneDrive project. So<exe_dir>/scriptsdoes NOT exist next to the binary on this machine;resolve_scriptfinds the workers only via the CWD-relative branch (sidecar.rs:19-21). Run from the project root, or setpredict_frag.sidecar_script_dir(and it is reused for rescore/MBR) to an absolute path, e.g./opt/mumdia/scriptsin the Docker configs. If none of the three candidates exist it returns the CWD-relative path so the eventual spawn error names it (sidecar.rs:32). - UTF-8.
run_deeplc/run_deeplc_finetunepassutf8=true; the PIN and entrapment sidecars setPYTHONUTF8=1directly. MS2PIP and MBR do not (sidecar.rs:63, 99, 152, 212). - Determinism (
docs/18_findings_and_decisions.md, determinism contract). MS2PIP predictions are deterministic regardless of process count (ms2pip_worker.py:40-41). DeepLC predict is deterministic given fixed weights. DeepLC fine-tune is nondeterministic: no torch/numpy seed is set (CLAUDE.md and MEMORY both flag this), so the rewritten iRT and thus the whole downstream run vary.mokapot_worker.pypinsrng=0/random_state=0/np.random.seed(0), but withsolver=adam+ BLAS threading the NN scores drift slightly; logreg is near bit-exact.nn_rescore_worker.pyseeds torch/numpy per seed and the fold split is a content hash, but training is only approximately reproducible (useMUMDIA_NN_SEEDS>1to average out variance).entrapment_worker.pyGBM is reproducible (early_stopping=False,random_state=0); the NN variant is not.mbr_worker.pyis deterministic given--seed(np.random.default_rng(seed)for the permuted-RT null). - make_reverse_decoys scramble is deterministic and process-independent. The
re-scramble PRNG for the collision/palindrome path is a SplitMix64 (
splitmix,make_reverse_decoys.py:65) seeded from a process-independent FNV-1a hash of the stripped sequence (stable_seed,make_reverse_decoys.py:74, applied atmake_reverse_decoys.py:119), not Python's randomized builtinhash. The scrambled decoys for those few peptides are therefore reproducible across library-build runs without settingPYTHONHASHSEED. - DIA-NN index precondition. Both decoy builders and the importer re-sort by
precursor_mz(stable mergesort) and reassign contiguouscandidate_id, and sort fragments bycandidate_id, soindex.rs load()'s contiguous-id + m/z-sorted precondition holds. Do not reorder afterward.make_reverse_decoys.pyadditionally aborts if its residue-mass calculator disagrees with the library's own target fragment m/z by > 5 ppm at p99. - Fallback exists only when explicitly requested (rescore only). With
rescore.strict = falsea crashed or misconfigured rescore sidecar is logged and the run continues onnative_tda; the defaultstrict = truemakes the failure fatal. Verify the actual classifier in the scored artifact report. The predictor sidecars (MS2PIP, DeepLC, DeepLC fine-tune) and MBR have no such gate: any nonzero exit aborts the run (see Failure behavior). - MS2PIP charge coverage. MS2PIP emits singly-charged b/y only; charge-2
fragments keep the native heuristic intensity (
predict_frag.rs:357-363). - Entrapment worker needs both classes.
entrapment_worker.pyraisesSystemExitif the training set (all non-decoy rows) is single-class, i.e. no real-target or no entrapment PSMs (entrapment_worker.py:86-87); the Rust side already guards this (n_ent == 0falls back tonative_tda,rescore.rs:218-234). Its fold count isk = max(2, min(folds, n_groups))over thebase_peptide_idgroups (:89-90); folds whose training side is single-class are left NaN and filled by the final full-data model, which also scores every decoy (:97-108). - Sidecar device selection.
nn_rescore_worker.pyuses CUDA automatically whentorch.cuda.is_available(), else CPU (nn_rescore_worker.py:112); it is not a config knob. The DeepLC fine-tune stays on CPU unless--device cudais passed (whichrunnever does).mokapot_worker.pyandentrapment_worker.pyare scikit-learn/CPU only. - mokapot uses OOF scores only, with no fallback.
_oof_scores(mokapot_worker.py:136-163) merges mokapot's held-out target and decoy confidence tables and raisesRuntimeErrorunless they cover the PIN rows exactly once (mokapot_worker.py:152-159) with finite scores (mokapot_worker.py:161-162). There is deliberately no in-sample or fold-averaging fallback: an incomplete confidence table is a hard worker error, which the Rust caller then treats perrescore.strict(bail if strict, elsenative_tda). Confirm the OOF branch ran on real runs (it prints "using complete out-of-fold confidence scores",mokapot_worker.py:166).
- Add a predictor/rescorer sidecar. Add the enum variant in
config.rs(FragPredictorKind/RtPredictorKind/RescorerKind), aConfigfield for its interpreter, a dispatch arm inpredict_frag.rs/rescore.rs, and a thin client insidecar.rs(or reuserun_pin_sidecarif it consumes the PIN and emitscandidate_id+score). Keep the positional-CLI-plus-Parquet contract; do not add a JSON request file or a server. - Reuse the PIN contract. A new rescorer that reads the PIN and writes
candidate_id+score+q_valueneeds no new Rust plumbing beyond aRescorerKindarm callingrun_pin_sidecarwith its script name; theMUMDIA_NN_*env vars are already injected. - Change env probing.
doctor(main.rs:313-374) hard-codes the package list per interpreter and switches the rescore packages on the classifier:nn_torch->torch,numpy,pandas,pyarrow; every other classifier (mokapot, entrapment, percolator, native) ->mokapot,sklearn,numpy,pandas,pyarrow(main.rs:318-324);predict_frag.deeplc_python->deeplc,numpy,pandas;predict_frag.ms2pip_python->ms2pip,numpy,pandas(main.rs:327-336). An interpreter leftNoneprints[skip](native path). It probes withimportlib.util.find_specand reportsMISSING <pkgs>; notembr.pythonis not probed bydoctor. Update these lists when a worker's imports change somumdia doctorstays truthful. - Conda envs. The committed reproducible specs are
env/docker-rescore.yml(envrescore: mokapot 0.10.0 + ms2pip 4.0.0.dev9, py3.11) andenv/docker-deeplc.yml(envdeeplc: DeepLC 4.0 multitask at a pinned commit + CPU torch, py3.11); the Docker configs point interpreters at/opt/conda/envs/{rescore,deeplc}/bin/python(docker/config.dia.json,docker/config.diann-lib.json).env/mumdia-rescore.ymlis the minimal portable env for the mokapot logreg path (no torch/DeepLC/MS2PIP). On the developer machine the workers also import from the localpy312_mumdiaenv (general env: torch + mokapot + ms2pip + sklearn + pyarrow),ms2rescore(mokapot + MS2PIP), anddeeplc_mtfor fine-tune (the a2 build; NOTdeeplc_multitask/a1, whose predict crashes, norpy310_deeplc, whose ms2pip is broken). Anchor only the tool version and let pip resolve its scientific-Python graph, since exact old pins (pandas < 2) have no cp312 wheel. - nn_torch scaling. For a many-run experiment-wide rescore, force the streaming
backend with
MUMDIA_NN_STREAM=1(or rely on the 4 GB auto threshold) so peak RAM is one minibatch, not the whole PIN; the memmap sidecar file is written next to the output and deleted on exit (nn_rescore_worker.py:270-275).