Part of the MuMDIA developer documentation (see docs/README.md).
This subsystem produces the run-independent spectral library and the data structures that make fragment matching cheap. It has three parts:
-
predict-frag (Stage C,
stages/predict_frag.rs): turn concrete peptidoforms into a library of precursor and b/y fragment m/z with predicted fragment intensities and predicted iRT, keep the top-N fragments per candidate, and assign each candidate acandidate_idequal to its rank in precursor-m/z order. The output is two Parquet artifacts that are constant for the whole run (they do not depend on the mzML being searched). -
the library loader (
index.rs,Library::load): read those two Parquet artifacts back into a Structure-of-Arrays in-memory model, group fragments by candidate, build the bucketed peak-major inverted fragment index, and enforce the preconditions the matchers rely on. -
the matchers (
matchers/): given an observed peak m/z, a ppm tolerance, and an isolation-window candidate range, return the predicted fragments that fall within tolerance. Two backends exist: the default log-bin CSRfragindex(matchers/fragindex.rs, spec infragindex_spec.md) and the fallback bucketedLibrary::page_search. A naive band-join (matchers/naive.rs) is the correctness oracle, not a production path.
The predictor traits (predict.rs) and the sidecar file contract (sidecar.rs)
sit under predict-frag: they are the boundary between the native (zero external
dependency) intensity/RT models and the optional Python predictors MS2PIP and
DeepLC.
| path | role |
|---|---|
rust/mumdia/crates/mumdia/src/stages/predict_frag.rs |
Stage C: build the library (parse, fragments, intensity, iRT, top-N, sort, write Parquet) |
rust/mumdia/crates/mumdia/src/predict.rs |
RtPredictor/FragmentPredictor traits + NativeRt/NativeFrag fallbacks |
rust/mumdia/crates/mumdia/src/sidecar.rs |
Python sidecar clients (MS2PIP, DeepLC, DeepLC fine-tune, MBR) over the file contract |
rust/mumdia/crates/mumdia/src/index.rs |
Library (SoA model + bucketed inverted index), load() preconditions, page_search, candidate_range, deconvolve |
rust/mumdia/crates/mumdia/src/matchers/mod.rs |
matcher module tree; MatcherKind selects the backend |
rust/mumdia/crates/mumdia/src/matchers/binning.rs |
LogBins: log-space bin geometry (fragindex_spec Section 2.2) |
rust/mumdia/crates/mumdia/src/matchers/fragindex.rs |
FragIndex CSR index, SeedScratch epoch-stamped accumulator, probe_peak, equivalence-gate scorer |
rust/mumdia/crates/mumdia/src/matchers/naive.rs |
band-join reference for the equivalence gate |
rust/mumdia/crates/mumdia-core/src/constants.rs |
within_ppm (min-relative predicate), ppm_bounds (query-relative), PROTON |
fragindex_spec.md |
language-agnostic algorithm spec the fragindex matcher implements |
peptidoforms (Stage A2 output), read at predict_frag.rs:52-58:
| column | type | note |
|---|---|---|
id |
u32 | peptidoform id |
base_peptide_id |
u32 | stripped-peptide id |
peptidoform |
str | ProForma-lite string with UniMod names |
charge |
i32 | precursor charge |
label |
str | "target" or "decoy" |
protein |
str | protein accession |
fragment_library_precursors (schema ("fragment_library_precursors", 1),
schema.rs:13), written at predict_frag.rs:197-211:
| column | type |
|---|---|
candidate_id |
u32 (dense 0..N in precursor-m/z order) |
peptidoform_id |
u32 |
base_peptide_id |
u32 |
peptidoform |
str |
charge |
i32 |
precursor_mz |
f64 |
predicted_irt |
f32 |
label |
str |
protein |
str |
n_fragments |
i32 (kept fragment count after top-N) |
fragment_library_fragments (schema ("fragment_library_fragments", 1),
schema.rs:14), written at predict_frag.rs:212-223:
| column | type |
|---|---|
candidate_id |
u32 (foreign key into precursors) |
mz |
f64 (fragment m/z at its own frag_charge) |
predicted_intensity |
f32 |
name |
str (e.g. b2, y3) |
ion_type |
str (b or y) |
ordinal |
i32 (residue-count ordinal of the fragment) |
frag_charge |
i32 (1, or 2 when enabled) |
Each artifact also gets an ArtifactReport (predict_frag.rs:234-252) recording
row count, blake3 content hash, params (top_n, ms2pip_model, rt_predictor,
fragment_predictor), a shared stats map (candidates, fragments,
parse_errors, predict_frag.rs:226-229), and model_identity (the concatenated
RT + fragment model ids, predict_frag.rs:119). Both reports share the same
stats/model_identity; only rows differs per artifact (predict_frag.rs:239-243).
The schema ids are the constants artifact::FRAGMENT_LIBRARY_PRECURSORS /
artifact::FRAGMENT_LIBRARY_FRAGMENTS (schema.rs:13-14). run itself returns
(n_prec, n_frag) (predict_frag.rs:50,261).
The library-input path (--lib-precursors/--lib-fragments) skips Stage C and
feeds externally built Parquet with these exact schemas directly into
Library::load.
predict_frag::run is driven by PredictFragParams (predict_frag.rs:24-31):
peptidoforms (input path), out_precursors / out_fragments (the two output
paths), cfg (&PredictFragConfig), work_dir (scratch dir for sidecar
Parquet), and config_hash (carried for provenance).
Phase A: parse and enumerate fragments (predict_frag.rs:60-110). Each
peptidoform row is parsed with parse_peptidoform and fragmented independently,
so rows are mapped in parallel with rayon (into_par_iter, line 72). The closure
returns RowOut::Raw, RowOut::ParseErr, or RowOut::Empty. Fragment charges
are chosen per row: charge 1 always, charge 2 added when the precursor charge is
at least charge2_from_precursor_charge (predict_frag.rs:78-82). collect
preserves row order, and the sequential fold at predict_frag.rs:104-110
reproduces the exact serial raws order and n_parse_err count. The parsed form
is stored on the Raw struct so intensity/iRT assignment reuses it instead of
re-parsing (predict_frag.rs:45-47).
iRT assignment (assign_rt, predict_frag.rs:265). Native path calls
NativeRt::predict_irt per candidate. DeepLC path deduplicates by peptidoform
string (RT is charge-independent, predict_frag.rs:281-290), runs the sidecar
once over the unique set, then maps results back. Peptidoforms DeepLC returns no
prediction for are anchored at irt = 0.0 and counted; if any are missing a
tracing::warn! fires (predict_frag.rs:303-308). This is the DeepLC-miss iRT
warning: it makes the silent "unmatched peptidoform gets iRT 0.0" failure visible,
because an iRT-0 anchor collapses the RT window onto the gradient origin and
misplaces the candidate at extraction. The DeepLC branch requires deeplc_python
and errors otherwise (predict_frag.rs:275-277); its returned model id is the
hardcoded string "deeplc-4.0-mt" (predict_frag.rs:309), not a trait
identity() (the sidecar path has no RtPredictor impl to query).
intensity assignment (assign_intensities, predict_frag.rs:315). Native
path calls NativeFrag::predict_intensities. MS2PIP path runs the sidecar over
all candidates; an empty whole-map result is a hard error (bail,
predict_frag.rs:342-344). Per candidate, MS2PIP supplies charge-1 b/y
intensities keyed by (ion_byte, ordinal); charge-2 fragments (which MS2PIP does
not emit) fall back to the native model (predict_frag.rs:350-363). MS2PIP
values (TIC-fraction scale, roughly 0.02-0.3) and the native charge-2 fallback
(max-normalized, roughly 0.19-0.5) live on different scales, so each charge group
is max-normalized to its own peak before they compete for top-N slots
(predict_frag.rs:365-384); otherwise the larger-scale group would always win the
truncation. There are two distinct MS2PIP-miss fallbacks: a candidate MS2PIP
returns nothing for (absent from the map, or an empty per-candidate map) falls
back wholesale to native (predict_frag.rs:387-389), whereas a single charge-1
fragment whose (ion_byte, ordinal) key MS2PIP omits gets intensity 0.0, not
the native value (unwrap_or(&0.0), predict_frag.rs:359); a fragment at
0.0 can then be dropped by top-N. MS2PIP requires ms2pip_python and errors
otherwise (predict_frag.rs:325-327); its model id is format!("ms2pip-{model}")
(predict_frag.rs:392).
top-N and candidate_id (predict_frag.rs:121-139). For each candidate,
fragments are ranked by predicted intensity descending, truncated to
top_n_fragments, then re-sorted ascending to restore stored order; an in-place
forward-swap gather compacts the kept fragments without reallocating. Candidates
left with zero fragments are dropped (retain, line 139). Then raws is sorted
by precursor_mz with a stable sort_by (predict_frag.rs:142), and
candidate_id is assigned by enumerate over that order
(predict_frag.rs:174-175). This is the single most load-bearing invariant of
the whole subsystem: candidate_id is the dense precursor-m/z rank, which is what
lets the index recover the isolation-window candidate slice by binary search and
lets candidate_id directly index the dense accumulator.
Both native fallbacks are deterministic and Python-free, so the engine runs with
zero external dependencies. NativeRt (predict.rs:55-70) sums a self-derived
per-residue hydrophobicity coefficient (rt_coeff, predict.rs:27-53, clean-room,
not a borrowed vector), adds 0.01 * mod_mass per modified residue, and adds a
sqrt(length) term so very long peptides do not elute infinitely late; identity
is native-rt-v1. NativeFrag (predict.rs:75-105) weights y ions at 1.0 and b
ions at 0.75, scales by a mid-sequence positional factor 1 - 0.5*|ordinal-L/2|/(L/2)
(mid-sequence fragments are more intense), halves charge-2 fragments, then
max-normalizes the whole vector to its peak; identity is native-frag-v1. The
predictor traits RtPredictor / FragmentPredictor (predict.rs:13-22) each
expose predict* plus identity; only the native structs implement them (the
sidecar paths bypass the traits and emit their own id strings, above).
Sidecars follow a positional-CLI Parquet contract (no JSON request file): write an
input Parquet, invoke python script arg..., read an output Parquet keyed by id.
run_ms2pip(sidecar.rs:42-78): input columnsid(u32),peptidoform(str),charge(i32); output columnsid,ion_type(str),ordinal(i32),intensity(f32). Returnscandidate_id -> (ion_byte, ordinal) -> intensity;ion_byteis the first byte ofion_type(b'?'if empty,sidecar.rs:72). Argv is[input, output, model](sidecar.rs:63).run_deeplc(sidecar.rs:81-105): input columnsid,peptidoform; output columnsid,predicted_rt(f32). Returnsid -> predicted_rt. Argv is[input, output].run_deeplc_finetune(sidecar.rs:111-155): fine-tunes the DeepLC RT model on confident seed PSMs and writes a new output precursor table with updatedpredicted_irt; it does not modify the input library. Positional contractdeeplc_finetune.py <lib_in> <seed> <lib_out> --epochs E --patience P --q-train Q --batch B. Invoked byrunbetween search-seed and RT calibration, not by predict-frag.run_mbr(sidecar.rs:162-213): match-between-runs transfer (Stage D3, needs= 2 runs). Positional contract
mbr_worker.py <scored_combined> <psms_csv> <out_transferred> [flags], wherepsms_csvis the per-run psms paths joined by,insourceorder; the launcher always passes--q-anchor,--min-anchor-runs,--q-transfer, and--seed, adds--out-scoredwhen an output-scored path is supplied, and adds--frag-csv/--consensus-corr-minonly when fragments are supplied and the threshold is > 0.run_worker(sidecar.rs:217-233) is the shared launcher; it bails if the process exits non-zero.utf8 = truesetsPYTHONUTF8=1andPYTHONIOENCODING=utf-8(DeepLC/Keras/torch crash on the Windows cp1252 console); it is on for the DeepLC and fine-tune calls, off for MS2PIP and MBR.
Library::load(precursors, fragments, bucket_size) reads both Parquet artifacts,
validates the label column (only "target"/"decoy" allowed, via
fdr::validate_labels, index.rs:65), and rebuilds the per-candidate fragment
arrays grouped and contiguous (index.rs:100-128), so cand_frags(cid) returns
three parallel slices (m/z, predicted intensity, name) for one candidate
(index.rs:209-218); n_candidates() is the candidate count (index.rs:204).
Each row becomes a Candidate (index.rs:23-35) with candidate_id,
peptidoform_id, base_peptide_id, peptidoform, charge, precursor_mz,
predicted_irt, protein, frag_start/n_frag (the slice bounds into the flat
fragment arrays), and is_decoy, which is derived from the label string
(label == "decoy", index.rs:122); the string label is not otherwise retained.
load then builds the bucketed inverted index (index.rs:160-188):
- Emit one
(frag_mz as f32, candidate_id, frag_int)entry per fragment. - Globally sort entries by fragment m/z with
par_sort_by(parallel stable sort, identical result to the serial stable sort,index.rs:170). - Chunk the sorted entries into fixed buckets of
bucket_size(floored at 1 viabucket_size.max(1),index.rs:173); record each bucket's first (== minimum) m/z inbucket_min; within each bucket sort bycandidate_id(index.rs:173-179). - Split into the three parallel arrays
idx_mz/idx_cid/idx_int.
page_search (index.rs:247) probes this index for an observed neutral m/z q.
It early-returns on a degenerate window or empty index (cand_hi <= cand_lo || idx_mz.is_empty(), index.rs:255-257). Otherwise ppm_bounds(q, tol_ppm) gives
the query window [lo, hi] (cast to f32); the first bucket is
partition_point(|m| m <= lo32).saturating_sub(1) and the last is
partition_point(|m| m <= hi32) over bucket_min (index.rs:258-268); within
each bucket, because idx_cid is ascending, two partition_point calls narrow to
the [cand_lo, cand_hi) slice (index.rs:275-276); a linear tail applies the
exact f32 m/z bound (index.rs:277-283). candidate_range (index.rs:238) turns
an isolation window into [lo, hi) over prec_mz by two partition_points (m < win_lo for lo, m <= win_hi for hi, so win_hi is inclusive and win_lo
exclusive).
FragIndex::build (fragindex.rs:46) is a two-pass counting sort into a CSR
layout keyed by log-space bin. It first derives the m/z range by scanning
lib.frag_mz for the min and max (fragindex.rs:61-70); if the library is empty
(no finite bound) it falls back to [1.0, 2.0] (fragindex.rs:71-74), and it
clamps the arguments to LogBins::new so mz_min >= 1.0 and mz_max > mz_min
(fragindex.rs:75). LogBins::new (binning.rs:27) asserts mz_min > 0 && mz_max >= mz_min and precomputes the geometry: delta = tol_ppm * 1e-6, bin
width w = ln(1 + delta), inv_w, ln_min, and `n_bins = floor(span * inv_w)
- 2
(the+2pads the top sobin+1never overflows).LogBins::bin(binning.rs:49) maps m/z tofloor((ln(mz) - ln_min) * inv_w), clamped to[0, n_bins-1](m/z <= 0 or <=mz_minmaps to bin 0). Pass 1 counts per-bin occupancy with the+1counting-sort offset (fragindex.rs:83-86); a prefix sum turns counts into CSR start offsets (fragindex.rs:88-90); pass 2 scatters postings in candidate-id order (fragindex.rs:98-110) sopost_candis ascending within every bin. Postings are Structure-of-Arrays (post_cand/post_mz/post_int/post_frag) so the verify hot loop streams onlypost_mz`.
FragIndex keeps its own copy of prec_mz and exposes n_cand()
(fragindex.rs:125), tol_ppm() (fragindex.rs:129), and a candidate_range
(fragindex.rs:137-141) with the same [lo, hi) semantics as
Library::candidate_range, so a caller holding only the index can still narrow to
the isolation window. probe_peak (fragindex.rs:152) early-returns on a
degenerate window (cand_hi <= cand_lo, fragindex.rs:159), then probes bins
bin(peak)-1 ..= bin(peak)+1 (clamped, fragindex.rs:162-165), narrows each bin
to [cand_lo, cand_hi) by binary search over the ascending post_cand
(fragindex.rs:172-174), and verifies each posting with the exact within_ppm
predicate in f64 (fragindex.rs:176-179). Its callback receives (cid, post_mz_f64, post_int, post_frag), where post_frag is the candidate-local
fragment ordinal that extract carries through directly (the bucketed path has to
recover it via local_frag_index). SeedScratch (fragindex.rs:191) is the epoch-stamped
dense accumulator: stamp[cc] records the last epoch a candidate was touched;
epoch is incremented before each scan (fragindex.rs:221) and starts at 0 so 0
is never a live epoch; on first touch the score is zeroed and the candidate pushed
to touched (fragindex.rs:227-232); after a scan only touched candidates are
read. The seed accumulates a fused (count, obs_sum) semiring where obs_sum
sums the observed peak intensity per matched posting (predicted intensity
deliberately discarded, fragindex.rs:234), and exposes the touched set plus
per-candidate count(cid) / obs_sum(cid) getters (fragindex.rs:241-253).
The free function score_scan_count_dot (fragindex.rs:260) is a separate,
non-SeedScratch scorer used only by the equivalence gate: it accumulates
(count, dot) per candidate over one scan, where dot is the sum over matched
postings of predicted_intensity * peak_intensity (both widened to f64, distinct
from SeedScratch's observed-only obs_sum), collects into a HashMap, and
returns the result sorted by candidate id (fragindex.rs:275-277). The naive
band-join scorer (naive.rs:16) computes the identical (count, dot) under the
same f32-rounded within_ppm predicate (naive.rs:30-33), which is what lets the
gate assert exact Count equality and near-exact Dot equality.
The +/-1 probe exactness. The correctness of probing only three bins rests on:
two m/z values within tolerance differ by at most one bin. Proof sketch: within
tolerance means |ln(a) - ln(b)| <= w, one bin width, so the two points span at
most two adjacent bins (binning.rs test within_tol_pairs_are_at_most_one_bin_apart,
fragindex_spec.md Section 2.2). One subtlety makes this exact in the
implementation: posting m/z is stored f32, so build bins each posting by the
same f32-rounded value the verify uses (fragindex.rs:85 and
fragindex.rs:102), not the raw f64. Binning by raw f64 while verifying the f32
value could place a posting two bins from the peak and silently drop a
boundary-straddling within-tolerance pair. The test
probe_finds_within_tol_across_bin_boundaries (fragindex.rs:429) sweeps the m/z
range to guard this.
The bucketed fallback vs fragindex predicate. The two backends do not use the
same tolerance predicate. page_search uses ppm_bounds (a window symmetric
around the query q, constants.rs:78). fragindex uses within_ppm (a
min-relative predicate, hi - lo <= tol_ppm*1e-6*lo, constants.rs:92). These
differ at the tolerance edge, so a handful of edge pairs are accepted by one and
not the other. On AIF full-range-window data the predicate difference shifts
identifications enough to matter, which is why the bucketed path is retained for
A/B (config.rs:30-35). The fragindex probe_peak and its naive band-join both
verify under within_ppm on f32-rounded m/z (naive.rs:30), so the equivalence
gate is not contaminated by storage precision.
Both stages build the fragindex backend only when MatcherKind::Fragindex is
selected and otherwise fall through to the bucketed Library path.
search-seed: FragIndex::build at search_seed.rs:56-57; fragindex path
seed_fragindex_windows parallelizes across isolation-window groups with a
per-thread SeedScratch and a deterministic total-order merge
(search_seed.rs:313-409); bucketed path uses page_search
(search_seed.rs:77). extract selects via the probe_matched helper
(extract.rs:42-59): fragindex carries the true generating fragment ordinal in
post_frag, whereas the bucketed path recovers the ordinal by nearest stored m/z
(Library::local_frag_index, index.rs:222).
| name | file:line | what it does |
|---|---|---|
PredictFragParams |
predict_frag.rs:24 |
Stage C entry args: in/out paths, cfg, work_dir, config_hash |
predict_frag::run |
predict_frag.rs:50 |
Stage C entry: parse, fragment, assign intensity/iRT, top-N, sort, write; returns (n_prec, n_frag) |
Raw |
predict_frag.rs:34 |
one candidate pre-assignment; caches the ParsedPeptidoform so RT/intensity reuse the parse |
assign_rt |
predict_frag.rs:265 |
native or DeepLC iRT; emits the DeepLC-miss warning; DeepLC id "deeplc-4.0-mt" |
assign_intensities |
predict_frag.rs:315 |
native or MS2PIP intensity with per-charge-group normalization + native charge-2 fallback; MS2PIP id "ms2pip-{model}" |
RtPredictor / FragmentPredictor |
predict.rs:13 / predict.rs:19 |
predictor traits (predict + identity); implemented only by the native structs |
NativeRt |
predict.rs:25 |
additive retention-coefficient model + sqrt(len) + 0.01*mod term, identity native-rt-v1 |
NativeFrag |
predict.rs:73 |
heuristic b/y intensity model (y=1.0, b=0.75, mid-seq positional, charge-2 x0.5), max-normalized, identity native-frag-v1 |
resolve_script |
sidecar.rs:20 |
locate a worker script (CWD, exe dir/dir, exe dir/scripts, else CWD-relative) |
run_ms2pip |
sidecar.rs:42 |
MS2PIP client; in id/peptidoform/charge, out id/ion_type/ordinal/intensity; returns cid -> (ion_byte, ordinal) -> intensity |
run_deeplc |
sidecar.rs:81 |
DeepLC client; in id/peptidoform, out id/predicted_rt; returns id -> predicted_rt |
run_deeplc_finetune |
sidecar.rs:111 |
DeepLC multitask fine-tune; deeplc_finetune.py <lib_in> <seed> <lib_out> + epoch/patience/q-train/batch flags (called by run, not predict-frag) |
run_mbr |
sidecar.rs:162 |
MBR transfer (Stage D3); mbr_worker.py <scored> <psms_csv> <out> + flags |
run_worker |
sidecar.rs:217 |
shared launcher; bails on non-zero exit; utf8 sets PYTHONUTF8/PYTHONIOENCODING |
Candidate |
index.rs:23 |
one library row in SoA; is_decoy derived from the label string |
Library |
index.rs:37 |
SoA candidate + fragment model plus the bucketed inverted index |
Library::load |
index.rs:54 |
read Parquet, validate labels, group fragments, build index, enforce preconditions |
Library::n_candidates / cand_frags |
index.rs:204 / index.rs:209 |
candidate count; per-candidate (m/z, intensity, name) slices |
Library::page_search |
index.rs:247 |
bucketed probe: bucket select -> candidate slice -> f32 ppm verify |
Library::candidate_range |
index.rs:238 |
isolation window -> [lo, hi) over prec_mz |
Library::local_frag_index |
index.rs:222 |
nearest-stored-m/z fragment ordinal (bucketed path only) |
deconvolve |
index.rs:291 |
z-charged peak m/z -> neutral m/z, in f64 |
LogBins / LogBins::bin |
binning.rs:11 / binning.rs:49 |
log-space bin geometry and mapping |
FragIndex::build |
fragindex.rs:46 |
two-pass counting-sort CSR build at a fixed tolerance; derives m/z range from the library |
FragIndex::probe_peak |
fragindex.rs:152 |
+/-1 bin probe + within_ppm verify, candidate-window narrowed; callback (cid, mz, int, frag) |
FragIndex::candidate_range / n_cand / tol_ppm |
fragindex.rs:137 / :125 / :129 |
index-side isolation-window narrowing + accessors |
SeedScratch |
fragindex.rs:191 |
epoch-stamped dense (count, obs_sum) accumulator; touched/count/obs_sum getters |
score_scan_count_dot (fragindex / naive) |
fragindex.rs:260 / naive.rs:16 |
equivalence-gate scorers, dot = predicted*observed, under an identical predicate |
within_ppm / ppm_bounds |
constants.rs:92 / constants.rs:78 |
min-relative vs query-relative tolerance predicates |
PredictFragConfig (config.rs:252-282, #[serde(default, deny_unknown_fields)],
so the struct was pruned to exactly these fields and unknown keys are rejected):
| field | default | effect |
|---|---|---|
predictor |
Native (FragPredictorKind) |
native heuristic vs Ms2pip sidecar for fragment intensities |
rt_predictor |
Native (RtPredictorKind) |
native additive model vs Deeplc sidecar for iRT |
charge2_from_precursor_charge |
2 |
precursor charge at/above which charge-2 fragments are added (was 3; lowered to keep the ~16% of charge-2 precursors' doubly-charged transitions) |
top_n_fragments |
6 |
fragments kept per candidate after intensity ranking (top-6 is standard DIA) |
ms2pip_model |
"HCD" |
MS2PIP model name passed as argv |
ms2pip_python |
None |
interpreter for the MS2PIP sidecar; required when predictor=ms2pip, else the stage errors |
deeplc_python |
None |
interpreter for the DeepLC sidecar; required when rt_predictor=deeplc, else the stage errors |
sidecar_script_dir |
"scripts" |
directory searched by resolve_script for the worker scripts |
Matcher selection (both stages default to Fragindex):
| field | default | effect |
|---|---|---|
MatcherKind |
Fragindex (config.rs:36-42) |
Fragindex (log-bin CSR) or Bucketed (Library::page_search) |
search_seed.matcher |
Fragindex (config.rs:300,316) |
backend for the seed search |
search_seed.fragment_tol_ppm |
20.0 (config.rs:288,312) |
tolerance the seed index is built at |
extract.matcher |
Fragindex (config.rs:460,534) |
backend for extraction |
extract.bucket_size |
8192 (config.rs:446,530) |
fixed bucket size of the bucketed Library index (fragindex ignores it) |
- candidate_id contiguity.
candidate_idmust be the dense range0..Nin precursor-m/z-ascending row order. predict-frag guarantees it (predict_frag.rs:142,174);Library::loadre-checks it and bails with a clear message (index.rs:78-87);FragIndex::buildasserts it (fragindex.rs:50-56). An external library fed in unsorted or unindexed (for exampleimport_diann_lib.pyoutput not passed throughmake_reverse_decoys.py) fails here rather than silently misgrouping fragments. - precursors ascending by m/z.
Library::loadverifiesprec_mzis non-decreasing and bails otherwise (index.rs:135-146); thecandidate_rangebinary search assumes it. - fragment foreign key. A fragment row referencing
candidate_id >= Nis a hard error (index.rs:92-96). - both label classes must be present. A library with zero targets or zero
decoys makes target-decoy q-values meaningless, so
loadnow bails at library load with a clear message rather than completing a long search on an invalid null (index.rs:147-158). The label column is also validated: any value other than"target"/"decoy"is rejected up front byfdr::validate_labels(index.rs:65,fdr.rs:127). - total_frags must fit u32.
FragIndex::buildasserts this (fragindex.rs:58) because posting indices are u32. - f32 posting m/z, f64 math. Index/posting m/z is stored f32; the ppm verify
widens to f64 (
fragindex.rs:176-177,index.rsmodule docs). f32 ULP is ~0.12 ppm, 200-400x below the 20-50 ppm regime, so storage precision is not a gate disagreement source; build and verify use the same f32-rounded value. - per-posting accumulation. A candidate with two fragments within tolerance of
one peak counts twice; a peak within tolerance of two candidate fragments counts
twice (fragindex_spec Section 1.4). Do not deduplicate. Tests
two_frags_one_peak_counts_both(fragindex.rs:371) and the count assertion inequivalence_gate_vs_naive(fragindex.rs:356) guard this. - epoch, not value, for first touch. First touch is
stamp[cc] != epoch, neveracc[cc] == 0; a legitimate zero score would otherwise be misclassified (fragindex.rs:227, spec Section 3.5). - determinism. predict-frag maps rows in parallel but
collect+ a sequential fold reproduce the serial order; the precursor-m/z sort is stable and top-N is a deterministic ranking;Library::loaduses a parallel stable sort equal to the serial one;FragIndex::buildis fully serial (candidate-order scatter, no hashing);seed_fragindex_windowsmerges partial results in a total order independent of thread/group order (search_seed.rs:392-408).SeedScratchsumsobs_sumin the caller's fixed peak order andtouched()is in first-touch order, so callers sort before any float reduction (fragindex.rs:239-243). - DeepLC nondeterminism and the iRT-0 anchor. The DeepLC sidecar and fine-tune
are not seeded, so iRT values vary run to run. Any peptidoform DeepLC does not
return lands at iRT 0.0; the warning at
predict_frag.rs:303-308reports the count so a large miss is visible rather than silent. - predicate mismatch between backends.
page_search(query-relativeppm_bounds) andfragindex(within_ppm) accept slightly different edge pairs; switchingMatcherKindcan move a small number of identifications. This is expected, not a bug (config.rs:30-35). naive.rsis not production. It is O(C x frags x peaks), used only as the equivalence oracle.
- Add a fragment or RT predictor. Implement
FragmentPredictor/RtPredictor(predict.rs:13,19) for a native model, or add a variant toFragPredictorKind/RtPredictorKind(config.rs:90,79) plus a branch inassign_intensities/assign_rt. Match the existingidentity()convention; the id flows intomodel_identityand the artifact report. Keep the native fallback path intact so the engine still runs with no Python. - Add a sidecar. Follow the positional-CLI file contract in
sidecar.rs: write an input Parquet, invokerun_worker(python, script, &[argv...], utf8), read an output Parquet keyed by id. Useresolve_scriptso a deployed binary finds the worker regardless of CWD. Setutf8 = truefor Keras/torch workers (they crash on the Windows cp1252 console). Never hardcode the interpreter path; it comes from config (ms2pip_python,deeplc_python, or the rescore/finetune equivalents). - Change the matcher. New backends go under
matchers/behind aMatcherKindvariant. Any new backend must pass the equivalence gate againstnaive.rsatK = Cunder the samewithin_ppmpredicate (test pattern infragindex.rs:331) before its speed is trusted. Before attempting a fragindex optimization, readfragindex_spec.mdSection 5: cache-blocking, accumulator prefetch, radix partitioning, bin-major inversion, and distinct-m/z dedup were all measured null or negative in the realistic DIA regime. The real levers are top-N reduction (already applied,top_n_fragments) and per-window parallelism (already applied inseed_fragindex_windows). - Change fragment charges or top-N.
charge2_from_precursor_chargeandtop_n_fragmentsare the two knobs; both are sensitivity/speed tradeoffs. Lowering top-N removes collisions roughly proportionally (spec Section 5.5) and shrinks the library; it does not change matcher correctness. - Feeding an external library. It must satisfy the
load()preconditions (densecandidate_idin precursor-m/z order, ascendingprec_mz, decoys present). The decoy-builder scripts (make_reverse_decoys.py) sort and reindex to satisfy them; do not bypass that step.