Part of the MuMDIA developer documentation (see docs/README.md).
Stage B (mumdia rt-im-train, PLAN.md Stage B) turns the run-independent
predicted iRT carried on each library candidate into a per-run predicted
retention time in seconds, and derives a per-candidate RT acceptance window that
the extractor uses to bound its scan search. It is the bridge between the
library (built once, run-independent) and this run's chromatography.
The stage does two things:
- Fit a calibration map
predicted_irt -> observed_rtfrom the confident seed PSMs of this run (a linear least-squares fit whenever at least two anchors exist; a LOESS local-linear smoother when configured and enough anchors are present). With fewer than two target anchors no trustworthy mapping exists, so the calibrated RT is marked unavailable and the windows are left unbounded. - Set an RT half-window from the residual distribution of that fit
(residual percentile times a multiplier), and emit
rt_lo/rt_hiaround the calibrated RT of every library candidate.
An optional pre-step, wired into the run orchestrator rather than into this
stage, fine-tunes the DeepLC multitask model on the same confident seed PSMs and
writes a new precursor-library table with updated predicted_irt before Stage B
reads it. The input library is unchanged. The fine-tune is a Python sidecar and
is nondeterministic. It is default-off.
Ion mobility (IM) is stubbed. The MVP is 3D, so the IM columns are always written null; there is no IM calibration or IM window.
| path | role |
|---|---|
rust/mumdia/crates/mumdia/src/stages/rt_im_train.rs |
The stage. Joins iRT to seed PSMs, fits calibration, computes windows, writes run_windows.parquet + cal.json. |
rust/mumdia/crates/mumdia/src/calibrate.rs |
Calibration math: linear_fit, the Loess local-linear smoother, and percentile. Shared, no external deps. |
rust/mumdia/crates/mumdia/src/stages/run.rs |
Orchestrator. Runs the optional DeepLC fine-tune between search-seed and this stage, then calls rt_im_train::run (see run.rs:242-291). |
rust/mumdia/crates/mumdia/src/sidecar.rs |
run_deeplc_finetune (sidecar.rs:110-155): the file-contract client that invokes the fine-tune worker. |
scripts/deeplc_finetune.py |
The fine-tune worker: transfer-learns DeepLC 4.0 on the seed and writes a new library parquet with replaced predicted_irt. |
rust/mumdia/crates/mumdia-core/src/config.rs |
RtImTrainConfig (config.rs:324-367), CalibrationMethod enum (config.rs:56-61), and the load-time validation that rejects calibration_method=none (config.rs:1030-1036). |
rust/mumdia/crates/mumdia-core/src/schema.rs |
artifact::RUN_WINDOWS = ("run_windows", 1) (schema.rs:16). |
fragment_library_precursors.parquet (the run-independent library; in
library-input mode this is the imported speclib, and when finetune_deeplc is
set it is the _ft rewrite). Columns read (rt_im_train.rs:78-79):
| column | type | use |
|---|---|---|
candidate_id |
u32 | join key; also the output row identity |
predicted_irt |
f32 | the value calibrated to observed RT |
The library is the single source of truth for iRT (rt_im_train.rs:75-83): both the
training anchors and the applied calibration read predicted_irt from this same
table, so a patched or fine-tuned library iRT is used consistently for fit and
apply.
seed_psms.parquet (from search-seed). Columns read (rt_im_train.rs:88-93):
| column | type | use |
|---|---|---|
candidate_id |
u32 | join to library predicted_irt |
base_peptide_id |
u32 | best-per-peptide grouping key |
spectrum_q |
f64 | confidence gate (< q_train) |
score |
f64 | picks the best PSM within a base_peptide_id |
observed_rt |
f64 | the calibration target (seconds) |
label |
str | only rows equal to "target" anchor the fit |
run_windows.parquet (artifact::RUN_WINDOWS, version 1). One row per
library candidate. Columns (rt_im_train.rs:266-277):
| column | type | meaning |
|---|---|---|
candidate_id |
u32 | library candidate identity |
rt_pred_cal |
f64 | calibrated predicted RT (seconds); NaN when calibration is unavailable (fewer than two anchors) |
rt_lo |
f64 | rt_pred_cal - width (window lower bound, seconds); negative infinity when calibration is unavailable |
rt_hi |
f64 | rt_pred_cal + width (window upper bound, seconds); positive infinity when calibration is unavailable |
im_pred_cal |
f64, nullable | always None (3D MVP) |
im_lo |
f64, nullable | always None |
im_hi |
f64, nullable | always None |
The unbounded row (NaN, -inf, +inf) is materialized by candidate_window
(rt_im_train.rs:65-70) whenever no calibrated RT or window width is available; the
infinite bounds make the downstream extractor scan the full isolation-window RT
range (recall-safe) rather than a numeric window.
cal.json (side artifact, written with mumdia_io::json::write_json,
rt_im_train.rs:290-302). Fields:
| field | meaning |
|---|---|
method |
"loess" if the LOESS path was used, "linear" if the linear map was used, or "unavailable" when fewer than two anchors exist (rt_im_train.rs:279-285) |
slope, intercept |
the linear fit coefficients, computed only when at least two anchors exist and therefore serialized as null when calibration is unavailable (rt_im_train.rs:286-287) |
w_rt |
the global RT half-window (seconds), null when the windows are unbounded |
p_rt |
the residual percentile used |
multiplier |
rt_window_multiplier |
n_train |
number of calibration anchors |
calibration_status |
"loess", "linear", "fallback_fixed", or "insufficient_anchors_unbounded" |
<run_windows>.report.json (ArtifactReport, rt_im_train.rs:309-321) records
logical_name/schema_name/schema_version (all from artifact::RUN_WINDOWS),
stage = "rt-im-train", rows, the blake3 content hash of run_windows.parquet,
params (q_train, p_rt, method as the Debug form of calibration_method),
stats (n_train, w_rt, calibration_status), and elapsed_ms. model_identity
is None (this stage applies no serialized model, rt_im_train.rs:318). Params
stats is a BTreeMap, so its key order is stable.
The stage entry point is run(p: RtImTrainParams) (rt_im_train.rs:72). Standalone
it is invoked as mumdia rt-im-train --seed-psms <p> --library-precursors <p> --out-windows <p> --out-cal <p> [--config <p>] (the RtImTrain subcommand,
main.rs:88-99, dispatched at main.rs:477-494); the run orchestrator calls the
same run function directly (run.rs:284-291). Both call sites build the config
hash and pass it as config_hash, but the stage never reads it (see gotchas).
Read the library precursors and build irt_by_cid: HashMap<u32, f64> mapping
candidate_id -> predicted_irt (rt_im_train.rs:80-83). This is the only source of
iRT; both training and application use it.
Read the seed PSMs and build best_per_pep: HashMap<u32, (score, irt, rt)>,
keyed by base_peptide_id (rt_im_train.rs:95-120). A seed row enters the pool only
if:
spectrum_q,score, andobserved_rtare all finite andspectrum_q < q_train(rt_im_train.rs:97-103); a non-finite field or a row at or above the threshold is skipped, andlabel == "target"(rt_im_train.rs:106); a decoy anchor would inject a random iRT/RT pair into the fit, and- its
candidate_idresolves to a finite librarypredicted_irt(rt_im_train.rs:109-113); a missing or non-finite library iRT is skipped.
Within a base_peptide_id, the highest-score PSM wins (rt_im_train.rs:117-118),
so each confident peptide contributes exactly one (predicted_irt, observed_rt)
anchor. sorted_anchor_vectors (rt_im_train.rs:54-60) then sorts the anchors by
base_peptide_id and splits them into the parallel vectors train_irt/train_rt
(rt_im_train.rs:121), with n_train = train_irt.len() (rt_im_train.rs:122). Sorting
before any float reduction makes the fit order-stable (see the determinism note in
the gotchas).
Calibration is attempted only when calibration_available = n_train >= 2
(rt_im_train.rs:127): zero or one point cannot define a useful mapping across the
gradient. When available, the linear fit is computed: let (slope, intercept) = linear_fit(&train_irt, &train_rt) (rt_im_train.rs:128-132); otherwise slope and
intercept are both NaN. LOESS is used only when calibration is available, the
method is CalibrationMethod::Loess, and there are at least
min_seed_for_calibration anchors (rt_im_train.rs:133-135); it is fit with
Loess::fit(&train_irt, &train_rt, loess_span, 200) (200 grid points,
rt_im_train.rs:136-140).
predict is a closure (rt_im_train.rs:142-150): it returns NaN when calibration
is unavailable, otherwise loess.predict(irt) when a LOESS model exists, else
slope * irt + intercept. The linear coefficients are therefore both the primary
map (Linear method) and the extrapolation fallback (LOESS outside its training
range; see below).
The math:
linear_fit(calibrate.rs:6-28) is ordinary least squares. Withn = xs.len():slope = (n*Sxy - Sx*Sy) / (n*Sxx - Sx*Sx)andintercept = (Sy - slope*Sx) / n. Degenerate guards: fewer than 2 points returns(0, mean(ys))(a constant map, calibrate.rs:8-16); a near-zero denominator (allxequal) returns(0, Sy/n)(calibrate.rs:22-24).Loess::fit(calibrate.rs:42-83) first computes the global linear fit as the extrapolation fallback (calibrate.rs:43), sorts the points byx(calibrate.rs:45-48), and if fewer than 4 points are present just fills the grid from the linear line (calibrate.rs:50-66). Otherwise the local window size isk = clamp(ceil(span * n), 3, n)(calibrate.rs:67) and it evaluates a local-linear regression on a uniform grid ofgrid_npoints spanning[min(x), max(x)](calibrate.rs:69-76).local_linear(calibrate.rs:107-153) selects theknearest anchors by walking outward from the insertion point (calibrate.rs:110-127), assigns each a tricubic weightw = (1 - d^3)^3withd = |x_i - x0| / dmax(calibrate.rs: 133-139), and solves a weighted least-squares line, returning the fitted value atx0.dmaxis the larger of the two window-edge distances, floored at1e-12to avoid a zero divide (calibrate.rs:128-130), and the weight is exactly 0 whend >= 1.0(calibrate.rs:134-139), so anchors past the window edge do not contribute. If the weighted system is degenerate, meaningsw < 1e-12(all weights vanished) orsw*swxx - swx^2near zero (calibrate.rs:146-149), it falls back to the global line.Loess::predict(calibrate.rs:87-102) uses the linear fallback forxat or outside the grid ends, and also when the grid has fewer than 2 nodes (calibrate.rs:89-94), and otherwise linearly interpolates between the two bracketing grid nodes found bypartition_point(calibrate.rs:95-101). When the two bracketing nodes are within1e-12inxit returns the lower node'syrather than interpolating, guarding against a zero divide (calibrate.rs:98-99). Interpolating a precomputed grid makes bulk application over the whole library cheap.
min_anchors = min_seed_for_calibration.max(2) (rt_im_train.rs:157). The half-window
is then chosen by window_plan(n_train, min_anchors, fallback_rt_window_s)
(rt_im_train.rs:42-50, called at 158-159), which returns one of three WindowPlan
variants (rt_im_train.rs:30-40):
Unbounded(n_train < 2): no trustworthy iRT to RT mapping exists. The stage warns, setsw_rt = None, and uses status"insufficient_anchors_unbounded"(rt_im_train.rs:160-167). Every candidate then gets the unbounded window(NaN, -inf, +inf)so extraction scans the full isolation-window RT range.Fixed(fallback_rt_window_s)(2 <= n_train < min_anchors): a linear map exists but there are too few anchors to estimate its residual distribution. The stage warns and retains the configured broad fixed half-window with status"fallback_fixed"(rt_im_train.rs:168-175).Calibrated(n_train >= min_anchors): the absolute residuals|observed_rt - predict(irt)|are formed (rt_im_train.rs:177-181), and the global half-window isw_rt = percentile(resid, p_rt) * rt_window_multiplier, floored at 1.0 second (rt_im_train.rs:182). The status is"loess"or"linear"(rt_im_train.rs:183).
The anchor-count gate exists for a concrete failure mode documented in the code (rt_im_train.rs:152-156): with only a handful of anchors a linear fit passes almost exactly through them, so residuals are near zero, the percentile window collapses to the 1-second floor, and that floor then discards nearly every true co-elution downstream. The fixed fallback avoids that collapse; the unbounded case below two anchors avoids fitting a mapping from a single point at all.
When adaptive_rt_window is set (and n_train >= min_anchors, which guarantees the
Calibrated plan and therefore a non-null w_rt), the global w_rt is replaced by
a per-bin half-width (rt_im_train.rs:192-229). Anchors are binned into
nb = adaptive_rt_bins.max(1) equal-width bins over the calibrated-RT range
(rt_im_train.rs:202), with the bin index computed from
frac = ((cal - rt_min)/span).clamp(0.0, 0.999_999) so the maximum RT lands in
the last bin rather than out of range (rt_im_train.rs:207). Each bin's
half-width is its local residual percentile times the multiplier, clamped to
[lo_clamp, hi_clamp] where lo_clamp = rt_window_min_s.max(0.0) and
hi_clamp = fallback_rt_window_s.max(lo_clamp) (rt_im_train.rs:210-211). Empty
bins fall back to the global w_rt (rt_im_train.rs:215-216). Each candidate then
takes the width of the bin its calibrated RT lands in (rt_im_train.rs:248-253).
Degenerate case: when all calibrated anchor RTs are equal (rt_max <= rt_min) the
adaptive block yields None and the stage silently uses the global w_rt
(rt_im_train.rs:203, 224-226). The rationale (config.rs:353-361): a single fixed
window is simultaneously too wide for well-calibrated regions and too narrow for
poorly-calibrated ones. This knob is part of the sensitivity program and has not
passed the entrapment gate, so it stays default-off.
For each library candidate (rt_im_train.rs:246-264): calibrated_rt = calibration_available.then(|| predict(irt)) (rt_im_train.rs:247), the width is
either the adaptive per-bin value or the global w_rt (rt_im_train.rs:248-255), and
candidate_window(calibrated_rt, width) (rt_im_train.rs:256) produces the row
(cal, cal - width, cal + width), or the unbounded (NaN, -inf, +inf) when either
value is absent. The three IM columns are pushed as None (rt_im_train.rs:261-263).
The table is written (rt_im_train.rs:266-277), cal.json is written
(rt_im_train.rs:290-302), and the artifact report is emitted (rt_im_train.rs:309-321).
This is not part of rt_im_train::run; it runs in the run orchestrator between
search-seed and Stage B, guarded by cfg.rt_im_train.finetune_deeplc
(run.rs:242-280). Preflight (run.rs:62-67) rejects finetune_deeplc unless
predict_frag.deeplc_python names a Python interpreter with DeepLC 4.0 multitask.
The seed PSMs are searched once on the base library before the fine-tune and are
reused as-is (the search-seed hyperscore does not depend on iRT), so the fine-tune
and Stage B both consume that same seed table; only the library's predicted_irt
changes in the newly written _ft table between them.
When enabled, the orchestrator resolves deeplc_finetune.py
(sidecar::resolve_script, run.rs:248-251), writes a fine-tuned library
fragment_library_precursors_ft.parquet (run.rs:252), and rebinds lib_p to that
file so both Stage B and extract read the fine-tuned iRT (run.rs:242-280). The
fine-tune is driven through run_deeplc_finetune (sidecar.rs:110-155), whose
positional CLI contract is deeplc_finetune.py <lib_in> <seed> <lib_out> --epochs E --patience P --q-train Q --batch B. The worker runs with both PYTHONUTF8=1 and
PYTHONIOENCODING=utf-8 set (the utf8 flag on run_worker, sidecar.rs:217-225)
because DeepLC/Keras crash on the Windows cp1252 console. run_worker spawns
python script arg... and returns an error if the process exits non-zero
(sidecar.rs:226-232); there is no JSON request file, only argv plus the parquet
column contract.
Inside the worker (scripts/deeplc_finetune.py): the reference set is the
confident target seed PSMs (label == "target", spectrum_q <= q_train, standard
residues only via is_std, deeplc_finetune.py:100-104). Note the gate is <=
q_train here, whereas the Rust anchor selection uses strict < q_train
(rt_im_train.rs:100), so the two reference sets can differ by the boundary rows.
The worker keys the reference dict by peptidoform string (ref[pf] = observed_rt,
deeplc_finetune.py:99-104), so it joins seed to library by peptidoform, not by
candidate_id as the Rust stage does, and it keeps one RT per peptidoform by
last-write-wins in iteration order rather than best-score. The batch size
auto-scales when --batch 0: min(512, max(16, n_ref // 30)), so each epoch runs
at least about 30 gradient steps; a fixed large batch underfits small references
(deeplc_finetune.py:111-117). deeplc.finetune(ref_psms, train_kwargs=...)
transfer-learns the model (deeplc_finetune.py:119-129); predictions come from
deeplc.predict(batch, model=ft_model) over the unique standard peptidoforms in
chunks of 100_000 (deeplc_finetune.py:138-154). DeepLC 4.0 multitask returns a 2D
array (one column per task head); agg reduces it to one iRT by averaging across
heads (a.mean(axis=1), deeplc_finetune.py:48-50, 151). Prediction is on the
DECOY_-stripped underlying sequence so decoys land on the same iRT scale as targets
(deeplc_finetune.py:44, 135-158). The rewritten column overwrites predicted_irt
in place in the output parquet (deeplc_finetune.py:156-159); peptidoforms with no
prediction, including non-standard ones, keep their original iRT
(preds.get(base_pf(pf), orig[i]), deeplc_finetune.py:156).
The worker also carries a documented OpenMP crash fix: numpy's OpenBLAS (GNU
OpenMP) and torch's Intel OpenMP coexist only under KMP_DUPLICATE_LIB_OK=TRUE,
and each spawns a full thread pool that oversubscribes the CPU during the
sustained backward pass and crashes the machine, so the worker pins OMP/BLAS to 1
thread and bounds torch to DEEPLC_FT_THREADS (default 8) before importing numpy
and torch (deeplc_finetune.py:6-13, 22-28, 82-91). deeplc is imported before
numpy for OpenMP load order (deeplc_finetune.py:32). Beyond the four flags the
engine passes, the worker accepts standalone-only flags the engine never sets, so
they take their defaults: --device {cpu,cuda} (cuda sidesteps the CPU OpenMP
crash entirely), --threads, --max-ref, --predict-limit, and --skip-predict
(deeplc_finetune.py:58-76).
The fine-tune sets no torch/numpy seed, so it is nondeterministic across runs (CLAUDE.md notes this). Its main use is library-input mode, where the base iRT is the imported DIA-NN library value rather than a native/DeepLC prediction, and a per-run fine-tune materially tightens the RT window.
| name | file:line | what it does |
|---|---|---|
RtImTrainParams |
rt_im_train.rs:19-26 | Input struct: seed PSMs path, library precursors path, output windows + cal paths, config, config hash. config_hash is a field but run never reads it (dead param; see gotchas). |
WindowPlan |
rt_im_train.rs:30-40 | Enum { Unbounded, Fixed(f64), Calibrated }: the three window regimes by anchor count. |
window_plan |
rt_im_train.rs:42-50 | Maps (n_train, min_anchors, fallback_width) to a WindowPlan. |
sorted_anchor_vectors |
rt_im_train.rs:54-60 | Sorts the best-per-peptide map by base_peptide_id into (train_irt, train_rt) so the fit is order-stable. |
candidate_window |
rt_im_train.rs:65-70 | Builds (cal, lo, hi); returns (NaN, -inf, +inf) when calibrated RT or width is absent. |
rt_im_train::run |
rt_im_train.rs:72-331 | The stage: join iRT, select anchors, fit, window, apply, write. |
linear_fit |
calibrate.rs:6-28 | OLS y = slope*x + intercept with degenerate-case guards. |
Loess |
calibrate.rs:31-37 | Grid-based local-linear smoother; carries a linear fallback for extrapolation. |
Loess::fit |
calibrate.rs:42-83 | Sorts anchors, builds a grid_n-point local-linear grid, k = clamp(ceil(span*n),3,n). |
Loess::predict |
calibrate.rs:87-102 | Grid interpolation inside range, linear extrapolation outside. |
local_linear |
calibrate.rs:107-153 | Tricubic-weighted local least squares at one point. |
percentile |
calibrate.rs:156-164 | Nearest-rank percentile: sorts a copy, rank = round(p.clamp(0,1)*(len-1)). Not interpolated. Empty input returns 0.0. |
CalibrationMethod |
config.rs:56-61 | Enum { Loess, Linear, None }; default Loess. None is rejected at load. |
RtImTrainConfig |
config.rs:324-367 | All Stage B config fields (below). |
run_deeplc_finetune |
sidecar.rs:110-155 | Sidecar client: deeplc_finetune.py <lib_in> <seed> <lib_out> [flags]. |
All fields live under rt_im_train in the config (RtImTrainConfig,
config.rs:324-367). The struct is #[serde(default, deny_unknown_fields)], so any
unknown key is a hard load error and every field has the default below. The config
surface was pruned: there are no IM calibration fields (IM is a stub), and
CalibrationMethod::None is a foot-gun rejected at load (config.rs:1030-1036) even
though the enum variant still exists.
| field | default | effect |
|---|---|---|
calibration_method |
loess |
loess, linear, or none. loess uses the smoother when anchors suffice, else falls back to linear. none is rejected by Config::validate (config.rs:1030-1036) because the code path silently degrades to linear. |
q_train |
0.01 |
Max spectrum_q for a seed PSM to become a calibration anchor (rt_im_train.rs:100) and to enter the DeepLC fine-tune reference (--q-train). |
p_rt |
0.95 |
Residual percentile for the RT half-window (rt_im_train.rs:182). 0.95 keeps ~95% of anchor residuals inside the window. |
rt_window_multiplier |
1.0 |
Scales the residual-percentile window (rt_im_train.rs:182). Larger widens the window: higher recall, more interference. |
min_seed_for_calibration |
50 |
Minimum anchors to (a) use LOESS instead of linear (rt_im_train.rs:135) and (b) trust the residual window instead of the fixed fallback (via min_anchors = max(this, 2), rt_im_train.rs:157). |
loess_span |
0.3 |
LOESS local-fit fraction; passed as span to Loess::fit (rt_im_train.rs:137). Fraction of anchors in each local window. |
fallback_rt_window_s |
120.0 |
Fixed half-window (seconds) when anchors are too few (WindowPlan::Fixed, rt_im_train.rs:168-175); also the upper clamp for adaptive bin widths (rt_im_train.rs:211). |
finetune_deeplc |
false |
Enable the DeepLC multitask fine-tune pre-step in run (run.rs:242). Requires predict_frag.deeplc_python (preflight, run.rs:62-67). |
finetune_epochs |
25 |
Fine-tune epoch cap (--epochs); early stopping usually halts sooner. Used only when finetune_deeplc. |
finetune_patience |
10 |
Early-stopping patience (--patience): epochs without val-loss improvement before stopping. |
finetune_batch |
0 |
Fine-tune batch size (--batch). 0 auto-scales to the seed size so each epoch has ~30+ steps (config.rs:348-351); a fixed large batch underfits small seeds. |
adaptive_rt_window |
false |
Per-region window instead of one global width (rt_im_train.rs:192-229). Sensitivity-program knob, not yet default-on. |
adaptive_rt_bins |
12 |
Number of equal-width calibrated-RT bins for the adaptive window (rt_im_train.rs:202). |
rt_window_min_s |
1.0 |
Lower clamp (seconds) for any adaptive half-window (rt_im_train.rs:210); mirrors the 1s floor on the global window. |
- IM is a stub.
im_pred_cal,im_lo,im_hiare alwaysNone(rt_im_train.rs:261-263). There is no IM calibration, no IM window, and no IM config field. Any 4D/diaPASEF work must add both the model and the columns. - Only targets anchor the fit. Decoy seed PSMs are excluded (rt_im_train.rs:106); admitting them would inject random iRT/RT pairs. Keep this filter if you refactor anchor selection.
- Library is the single iRT source. Training and application both read
predicted_irtfrom the same library table (rt_im_train.rs:75-83, 232-233). When the fine-tune writes its new table, the orchestrator rebindslib_pto the_ftfile so both this stage andextractsee the updated iRT. Do not reintroduce a second iRT source or imply that the original file was mutated. - Fewer than two anchors leaves the windows unbounded. With
n_train < 2no linear map is fit (slope/interceptareNaN),w_rtisNone, and every candidate gets(rt_pred_cal = NaN, rt_lo = -inf, rt_hi = +inf)with status"insufficient_anchors_unbounded"(rt_im_train.rs:42-50, 160-167). Downstream this is recall-safe: the extractor scans the full isolation-window RT range rather than a numeric window. Do not treatNaN/infinite rows as a bug. The feature stage neutralizes theNaNrt_pred_calsentinel explicitly:calibrated_rt_error(features.rs:271-277) returns a 0 RT error when eitherapex_rtorrt_pred_calis non-finite, so the sentinel never contaminates the feature matrix or the preliminary competition score. - The 1-second floor. The
Calibratedglobal window is floored at 1.0s (rt_im_train.rs:182). With too few anchors the fit is near-exact, residuals collapse, and this floor would discard true co-elutions, which is exactly why theFixedfallback exists for2 <= n_train < min_anchors(rt_im_train.rs:152-186). Do not remove the fallback branch. percentileis nearest-rank, not interpolated (calibrate.rs:156-164). It sorts a copy each call, so it is order-independent, but repeated calls on the same data re-sort. This is fine at Stage B sizes.- Calibration is order-deterministic (fixed).
best_per_pepis still aHashMap, butsorted_anchor_vectors(rt_im_train.rs:54-60, called at 121) sorts the anchors bybase_peptide_idbefore buildingtrain_irt/train_rt, solinear_fit's summation order (calibrate.rs:17-20) is fixed across processes andslope/intercept/w_rt/the calibrated RTs are reproducible.Loess::fitadditionally re-sorts byxinternally (calibrate.rs:45-48). This satisfies CLAUDE.md's determinism rule (ordered iteration where floats are summed); do not reintroduce a.values()-order reduction. The testanchor_vectors_are_sorted_by_base_peptide_id(rt_im_train.rs:337-352) guards it. - The fine-tune is explicitly nondeterministic.
deeplc_finetune.pysets no torch/numpy seed (CLAUDE.md), sofinetune_deeplc = truemakespredicted_irt, and therefore the whole run, non-reproducible. Treat it as an accuracy lever, not a deterministic default. slope/interceptare emitted incal.jsonwhenever calibration is available, including under LOESS (rt_im_train.rs:128, 286-287, 290-302). They are the LOESS extrapolation fallback, not dead values; do not assume they were unused whenmethod == "loess". They are serialized asnullonly when calibration is unavailable (n_train < 2), since the fit is not computed in that case.CalibrationMethod::Nonestill exists but is rejected at config load (config.rs:1030-1036). The stage would otherwise fall through to the linear path becauseuse_loessmatches onlyLoess(rt_im_train.rs:133-135). This fallthrough is a known correctness wart (see CLAUDE.md "Correctness").- Report schema version is 1 (
artifact::RUN_WINDOWS, schema.rs:16). Bump it if the column set changes. config_hashis a dead param in this stage. Both call sites build the blake3 config hash and pass it asRtImTrainParams.config_hash(run.rs:290, main.rs:492), butrunnever reads it (unlike stages that stamp it into their report). Do not rely on the report to carry the config hash for Stage B.- Anchor gate is
<, fine-tune gate is<=. The Rust anchor selection keeps seed rows withspectrum_q < q_train(rt_im_train.rs:100), while the DeepLC fine-tune worker keepsspectrum_q <= q_train(deeplc_finetune.py:102). The two confident-seed reference sets can therefore differ by the boundary rows. Keep this in mind when reasoning about why the fine-tune reference count and the calibration anchor count are not identical. - Fine-tune joins by peptidoform, Stage B joins by
candidate_id. The stage maps seed to library iRT throughcandidate_id(rt_im_train.rs:109-113); the worker maps seed observed RT to library peptidoforms through thepeptidoformstring (deeplc_finetune.py:99-104, 156). A library whose peptidoform strings do not match the seed's would silently fine-tune on nothing. - Test coverage.
calibrate.rshas three unit tests:linear_recovers_line(calibrate.rs:170-176),loess_tracks_nonlinear(calibrate.rs:178-185), andpercentile_basic(calibrate.rs:187-191). The stagert_im_train.rsnow has three unit tests covering the helper functions:anchor_vectors_are_sorted_by_base_peptide_id(rt_im_train.rs:337-352),sparse_anchor_policy_is_unbounded_only_below_two(rt_im_train.rs:354-365), andunavailable_calibration_emits_unbounded_window_and_nan_prediction(rt_im_train.rs:367-378). The fullrunbody (anchor selection over a real seed table, the LOESS fit path, and the adaptive window) is still exercised only in full runs.
- Add IM (4D). Populate
im_pred_cal/im_lo/im_hi(currentlyNoneat rt_im_train.rs:261-263) from an IM calibration analogous to the RT path (an IM2Deep-style model), and add IM config fields toRtImTrainConfig. The output columns already exist and are nullable, so downstream reads survive the transition.extractand the IM feature families must then consume them. - Change the calibration model. Extend
CalibrationMethod(config.rs:56-61) and branch in thepredictclosure setup (rt_im_train.rs:133-150). Keeplinear_fitas the universal fallback so a degenerate anchor set never panics. New models belong incalibrate.rsnext toLoess, with unit tests likeloess_tracks_nonlinear(calibrate.rs:178-185). - Tune the window policy. The window-derivation logic is localized at
rt_im_train.rs:152-229 (the
window_planmatch plus the optional adaptive block). New window strategies (for example a symmetric-vs-asymmetric window, or a learned per-charge width) should be config-gated and default-off, then validated against the entrapment holdout before becoming a default, per the sensitivity program (sensitivity_plan/NEXT_STEPS.md). - Swap the fine-tune worker. The contract is purely the CLI and the parquet
columns (
sidecar.rs:107-155,scripts/deeplc_finetune.py). A replacement worker need only accept<lib_in> <seed> <lib_out>plus the four flags and rewrite thepredicted_irtcolumn. Preserve the DECOY_-stripping behavior (deeplc_finetune.py:44, 135-156) so decoys stay on the target iRT scale, and set a seed if you want the fine-tune to be reproducible.