Update TrkPID to use track-based observables - #9
Conversation
oksuzian
left a comment
There was a problem hiding this comment.
PR Review Summary — "Update TrkPID to use track-based observables" (#9)
Reviewed at head 3935ad52 (2026-08-04). First review of this PR; no prior reviews or comments to carry forward. Scope: retrain TrkPID on MDC2025 flat e−/μ− samples with track-based observables (E/P, p(χ²), hit-time-vs-track-time slope), plus a new calo-free TrkOnlyPID package, ntuple-skimming tools that add the fitted dt/dz and dt/dt branches, and a slope-method comparison script. Note: MLTrain is outside the Offline C++ coding-standards scope; findings below are about workflow correctness, not style conformance.
Decision
- 🔴 Request changes — two S1s, each a one/two-line fix: the new
TrkOnlyPIDtrainer still uses themixed_float16policy that commite202463identified as incompatible with TMVA SOFIE and fixed inTrkPIDonly, and the version-tagged output renaming leftplot_historyreading a filename that is no longer written, so the defaultTrkPIDrun now ends inFileNotFoundError.
Findings
-
🟠 [S1]
TrkOnlyPIDkeeps themixed_float16policy your own SOFIE fix removed fromTrkPID- Evidence:
TrkOnlyPID/TrkOnlyPIDTrain.py:116setsset_global_policy('mixed_float16'). Commite202463("Update to float32 for TMVA SOFIE") changed exactly this line to'float32'— but only inTrkPID/TrackPIDTrain.py:126.TrkOnlyPIDwas added before that fix (1665f05) and never picked it up. - Impact: the trained/saved
TrkOnlyPIDmodel reproduces the configuration you already found breaks the ONNX→SOFIE conversion, so it will fail at export time once the ONNX tooling is available (the PR body notes export is deferred), likely after the training work is considered done. - Suggested fix:
'mixed_float16'→'float32'inTrkOnlyPIDTrain.py, matchinge202463.
- Evidence:
-
🟠 [S1] Default
TrkPIDrun crashes at the end:plot_historyreads a filename the versioned save no longer writes- Evidence:
train_modelnow savesPID_model_v{version}.kerasandtrain_history_v{version}.json(TrkPID/TrackPIDTrain.py:149-151, renamed ine7dc3a8), but the plotting block still callsplot_history("train_history.json", ...)(TrkPID/TrackPIDTrain.py:439). On main, save and read were both unversioned, so this is a regression introduced by the rename.*.jsonis gitignored, so in a fresh area the unversioned file never exists. - Impact: a full default invocation (import → train → export → plots) dies with
FileNotFoundErrorafter the training time has been spent;plot_historyandplot_modeloutput is lost. - Suggested fix:
plot_history(f"train_history_v{version}.json", ...).
- Evidence:
-
🟡 [S2]
TrkOnlyPIDsave/load names are asymmetric, so--skip-traincan never load what a training run wrote- Evidence:
3935ad5version-tagged the load path (TrkOnlyPID_model_v{version}.keras,train_history_v{version}.json,TrkOnlyPIDTrain.py:374-375) and the ONNX output, but the save path still writes unversionedTrkOnlyPID_model.keras/train_history.json(TrkOnlyPIDTrain.py:139-141);plot_historyat line 408 also reads the unversioned name. - Impact: train-then-
--skip-trainround trip fails unless files are renamed by hand; same class of drift as finding 2, mirrored. - Suggested fix: version-tag the save path (and the
plot_historyargument) to match the load path, asTrkPIDdoes.
- Evidence:
-
🟡 [S2] The new background-efficiency bisection in
make_resultshas no termination guarantee- Evidence:
TrkPID/TrackPIDTrain.py:169-174andTrkOnlyPID/TrkOnlyPIDTrain.py:159-164(new in this PR) loopwhile abs(eff - bkg_eff)/bkg_eff > tolerancewith no iteration cap.effis a step function of the threshold (quantized in units of 1/nbkg, with plateaus wherever predictions tie). - Impact: potential risk, not observed — if no achievable efficiency lands within 1% of the target (small test set, or tied/saturated sigmoid outputs, e.g. a degenerate training), the interval collapses onto a plateau and the script hangs forever.
- Suggested fix: replace the loop with
threshold = np.quantile(dataset.loc[dataset['label']==0, 'prediction'], 1 - bkg_eff)— exact, one line, and removes the failure mode; otherwise add an iteration cap.
- Evidence:
-
🟡 [S2]
perform_tz_fithas no guard on degenerate hit lists- Evidence: both
make_inputs.pyfiles fit every track unconditionally; with ≤2 hitscurve_fitraises, and with exactly 3 points minus 2 parameters the code is fine butdof = len(z) - 2reaches 0 at 2 hits →ZeroDivisionError;curve_fitcan also raiseRuntimeErroron non-convergence. - Impact: potential risk — one pathological track aborts the whole multi-file skim, losing the run. Tracks in these ntuples normally have ≫3 hits, so this is defensive, but the skim is the expensive step.
- Suggested fix: guard
len(hits) < 3(and wrap the fit in try/except), pushing a sentinel (e.g. NaN) for that track so downstream selection can drop it.
- Evidence: both
-
🟡 [S2] Duplication across
TrkPID/TrkOnlyPIDis already biting (simplification lens — does not gate approval)- Evidence: the two
make_inputs.pydiffer by one blank line and one comment; the two pairs of 150-line.fileslists are byte-identical;TrkOnlyPIDTrain.pyis ~85% identical toTrackPIDTrain.py(import_evtntuple,apply_cut,make_results, all plotting). Finding 1 is the concrete cost: a fix applied to one copy silently missed the other. - Suggested fix: not asking for a refactor in this PR — but consider one shared
make_inputs.py(the branch additions are identical) and one copy of the dataset file lists, with the trainers importing shared helpers as a follow-up.
- Evidence: the two
-
🟡 [S2]
TrkOnlyPID/README.mddocuments a feature the model no longer uses- Evidence: README lists "tracker hit dt/dz slope divided by the expected slope from the track fit assuming an electron mass" as a v0 input, but v0 features are
['nActiveFrac', 'nNullFrac', 'fitcon', 'dtdt_slope'](TrkOnlyPIDTrain.py:346) — the dt/dz ratio was replaced by the dt/dt slope in3935ad5, and the TrkPID README was updated while this one wasn't. The README also names the output "TrkOnlyPID.onnx" vs the actualTrkOnlyPID_v{version}.onnx. (The PR description likewise still says "dt/dz over the expected slope".) - Suggested fix: update the v0 feature list and output name; one line in the PR body noting the dt/dz→dt/dt switch would help future archaeology.
- Evidence: README lists "tracker hit dt/dz slope divided by the expected slope from the track fit assuming an electron mass" as a v0 input, but v0 features are
-
⚪ [S3]
ROOT.TMath.GausIdoes not exist- Evidence:
TrkPID/compare_hit_slopes.py:36callsTMath.GausIin the equal-sigma branch ofanalytic_gaussian_overlap. TMath has noGausIin any ROOT version (checked v5.34 and v6.16 headers on cvmfs and current master docs); the normal CDF isTMath::Freq. - Impact: nearly dead code — the branch runs only when the two fitted sigmas agree to
math.isclosedefault 1e-9 — but it wouldAttributeErrorif it ever fired. - Suggested fix:
return 2 * ROOT.TMath.Freq(z).
- Evidence:
-
⚪ [S3] Housekeeping batch (no action required for approval)
- Inherited "cosmic muon" wording is now wrong:
apply_cutdocstrings and the confusion-matrix/rate printouts in both trainers label the background "cosmic muons", but the samples are flat μ− from target stops. - Dead/unused:
time_mod(mod 1695) is computed and never read;branches_mcimportstrkmc/trksegsmcbut onlytrkmcsimis used (extra I/O);nMatActiveFracis derived and unused in both feature sets;pconv→pcov. - Typos: "Unknown training verion" (both trainers), "unnessary" (both READMEs).
- Pre-existing, noting only since the lines are nearby:
plot_ROCprints "Accuracy at this threshold" but the value is the TPR; the TrkPID README link[TrackPIDTrain.py](TrkPIDTrain.py)targets a nonexistent filename.
- Inherited "cosmic muon" wording is now wrong:
Questions (non-gating)
- The t–z / t–t fits in
make_inputs.pyuse everytrkhitsentry unconditionally;TrkStrawHitInfo.stateencodes activity. Is including fit-deactivated hits intended (they may carry discrimination for the wrong-mass hypothesis) or should the fit filter on state? tf.keras.layers.Input(..., batch_size=32)bakes a fixed batch of 32 into the ONNX signature (pre-existing on main, copied intoTrkOnlyPID). Does the SOFIE-generated inference in Offline evaluate with batch 32, or does per-track batch-1 inference needbatch_size=None/1 at export? Worth settling before the first real export since that step is being redone anyway.
Verified 🟢 (checked, no action needed)
- 🟢
make_inputs.pyfield usage verified againstEventNtuple/inc/TrkStrawHitInfo.hh:earlyendis anint(valid array index),etimeisTDCTimes, andtottdrift,ptoca,udt,pocaall exist with the semantics used; the dt/dt construction (fit of hit time vs track TOCA; slope ≈ 1 under the correct mass hypothesis) is coherent. - 🟢 The
SetBranchStatussequence is correct: heavy branches dropped,CloneTree(0)excludes them from the output,trkhitsre-enabled after cloning for reading only — the skimmed tree carries the six new fit branches but nottrkhits. - 🟢
TrkPIDkeeps thetrkcalohit.activerequirement;TrkOnlyPIDcorrectly drops it (tracker-only). - 🟢 MC-truth selection (
trkmcsim[...,0].pdg== 11/13, highest-rank match) matches the FlateMinus/FlatMuMinus samples; the electron-hypothesisdtdz_exp(m = 0.511 MeV) matches the documented intent. - 🟢 TrkPID README v1 feature list matches the implemented
version == 1features. - 🟢
.gitignoreadditions appropriately keep models, CSVs, ROOT files, and figures out of the repo.
Validation check
- Build/tests run: none — MLTrain has no CI, and this review did not execute the scripts (no rootana env in session). Verification is static: three-dot diff against merge-base
2e5b8c3, per-commit archaeology, and field-level checks against EventNtuple headers and ROOT (cvmfs + docs). - Config contract check: n/a (no FHiCL).
- Cross-repo consistency: the PR body's plan to move the dt/dz-type observable into KalSeed/TrkInfo (EventNtuple) is the right long-term home; nothing in Offline/EventNtuple is required for this PR to merge.
Residual risk
- The exported-model path (ONNX → SOFIE → Offline) is untested by construction (tooling unavailable); findings 1 and the batch-size question both live there and will surface at export time.
Author follow-ups
'mixed_float16'→'float32'inTrkOnlyPIDTrain.py(finding 1).- Version-tag
plot_history's filename inTrackPIDTrain.py:439and theTrkOnlyPIDsave path (findings 2–3). - Consider the
np.quantilereplacement for the threshold bisection and a hit-count guard inperform_tz_fit(findings 4–5). - Refresh
TrkOnlyPID/README.mdfor the dt/dt feature (finding 7).
This update includes:
To add the tracker hit dt/dz, I'm adding a branch to the EventNtuple tree for each track, as it's not yet in EventNtuple. I think this observable should ideally be added to the KalSeed data product and then naturally added to the TrkInfo struct in EventNtuple.
As the ONNX features aren't yet available in
pyenv rootanathe model has not been exported to ONNX or added to Offline processing yet.Input features and output predictions:
