Skip to content

Stability controls for the bfcorr BField domain walk - #224

Open
RobMina wants to merge 4 commits into
KFTrack:mainfrom
RobMina:bfcorr-domain-step-floor
Open

Stability controls for the bfcorr BField domain walk#224
RobMina wants to merge 4 commits into
KFTrack:mainfrom
RobMina:bfcorr-domain-step-floor

Conversation

@RobMina

@RobMina RobMina commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Bringing up bfcorr=true CentralHelix→CRV extrapolation for Mu2e cosmics exposed four ways the BField
"domain walk" can fail: an OOM, a multi-minute CPU spin, a lost fit, and an aborted event. They share
one root cause — the domain step has no lower bound and the domain set no upper bound — compounded by
the fact that the CentralHelix parameterization is singular as |B|→0 (p_T ∝ B/ω; the dPardB ~ bfrac/(1+bfrac) correction has a pole at bfrac = −1). Near-vertical cosmics leaving the DS bore just
past the tracker drive the walk straight into that region.

The failure modes

# Symptom Mechanism
1 OOM — RSS 0 → 6.7 GB in ~20 s, then killed Backward (CRV-bound) extrapolation of a near-vertical cosmic degenerates climbing into the DS bore: traj pieces shrink to ~4e-5 ns, reported momentum collapses unphysically (11.46 GeV → 11 MeV), rangeInTolerance returns a vanishing dt, and addDomain appends an effect + traj piece per step without bound.
2 Multi-minute CPU hang (flat memory) A tight BCorrTolerance drives createDomains' trange→0, so tstart stops advancing and the loop spins emitting deduped domains into a std::set.
3 Fit lostthrow std::invalid_argument("Invalid domains") extend builds extension domains in independent createDomains calls, each with its own ±0.5·trange margin; the blocks don't tile contiguously and createEffects requires abutting domains.
4 Whole event abortedthrow std::length_error("Empty PiecewiseTrajectory!") A degenerate seed yields zero domains, so the seed trajectory comes out empty; the throw is uncaught and art drops the event, corrupting the output entry count.

New Config knobs

knob default effect at default effect when set
mindtstep_ 0.0 ns max(rangeInTolerance, 0) == rangeInTolerance floor on the domain step; bounds the domain count where rangeInTolerance→0
minfield_ 0.0 T guarded by minfield_ > 0 stop the bfcorr extrapolation once sampled |B| drops below it, before the dPardB pole
domainmargin_ DBL_MAX ns max(x, x−DBL_MAX) == x confine domains — and the field sampling at their midpoints — to the active range ± margin
maxdomains_ UINT_MAX size() > UINT_MAX never true hard cap per fit; over-cap fits fail cleanly instead of building ~1e5 domains first
zerofield_extrap_ false legacy branch taken verbatim hand extrapolation off to geometric free-particle continuation outside the map / below minfield_

Always-on changes

Four changes are not behind a knob. Each only fires where the previous code had undefined behavior or an
uncaught throw, so no configuration reaches them on a well-behaved track:

  • detectorRange returns a null TimeRange when there are no active hits/xings, instead of constructing
    an inverted range that throws.
  • Empty domain set after the detector-range trim → soft-fail status, instead of dereferencing an empty
    container.
  • Empty seed trajectory → soft-fail status, instead of the uncaught length_error of Mat #4.
  • replaceDomains builds the replacement trajectory before clearing domains_/effects_, so a failed
    append leaves the track intact rather than half-mutated. Behavior-preserving on the success path.

Verification

Opt-in / no-effect-when-unconfigured. Mu2e Offline built twice against the same source base, differing
only by this branch versus its base 2011f34, run on identical input with no knob configured:

fit type sample result
LoopHelix (production reco/OnSpill.fcl defaults) 20,000 events, 3,356 tracks IDENTICAL — 230 numeric leaves × 13 branches, exact (rtol = 0)
LoopHelix, KalSeed BField domain bounds 367,182 bounds @ 17 sig. digits IDENTICAL
CentralHelix (cosmic reco) 887 events, 527 track segments IDENTICAL — 449 leaves / 81 branches
KinematicLine unaffected by construction: fits with bfcorr=false, and every change here is inside a bfcorr_ branch

Stability. Event 5160 under a 4 GB address-space cap, knobs off versus on, with nothing else changed:

knobs off (= current head) knobs on
peak RSS 3.2 GB (at the cap; ~6.7 GB uncapped) 1.0 GB
wall time 43.5 s (27% CPU — 672k page faults) 4.0 s
outcome failsFatalRootError finalizing a 367,596-domain runaway completes, exit 0; degenerate track bounded and dropped cleanly

RobMina and others added 4 commits July 20, 2026 08:33
…cosmics

Robustness fixes to the bfcorr=true CentralHelix domain machinery for near-vertical
cosmic tracks that exit the DS bore (|B| -> 0):

- Config: add mindtstep_ (min domain step), domainmargin_ (clamp domains to the
  active range +/- margin), and minfield_ (stop extrapolation before the dPardB
  pole). Defaults preserve legacy behaviour (margin = max(), minfield = 0).

- createDomains/extendDomains: floor the domain step at mindtstep_ so a vanishing
  rangeInTolerance in a high-gradient/low-momentum region can no longer spawn an
  unbounded number of micro-domains (CPU hang / OOM). Clamp domain bounds to the
  active range +/- domainmargin_ so a uniform-field track just past the tracker
  doesn't sample Bz->0 at a domain midpoint and blow the CentralHelix reference off
  by metres via the singular bfrac/(1+bfrac) correction.

- createEffects: connect only domains that actually abut, instead of throwing
  "Invalid domains" across a gap between separate contiguous domain blocks.

- extrapolate: break before |B| < minfield_, tested at the new domain midpoint
  (not the frontier), handing the field-free region to a straight line tail.

- convertSeed: guard against an empty fittraj_ when a degenerate active range yields
  zero domains -- createEffects would otherwise build a Measurement against an empty
  PiecewiseTrajectory and throw std::length_error, aborting the whole art event.
  Treat it as an unfittable track (outsidemap) so the module drops it cleanly.

Includes temporary domain-count / empty-seed diagnostics (marked "remove before PR").

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…gnostics

- Config: new maxdomains_ (default unlimited, preserving legacy behaviour) printed in
  operator<<. A hard cap on the number of BField domains a single fit may accumulate.

- Track.hh createDomains/extendDomains: when domains exceed maxdomains_, throw -- caught
  by the existing fit()/processEnds() handlers, recording the fit as failed so the
  unusable track is dropped. A diverging low-momentum track can otherwise build ~1e5
  domains (each a KKDW effect + traj piece) -> wasted CPU + ~GB memory before being
  dropped anyway. Validated over 112 cosmic files: usable tracks peak at 368 domains,
  runaways at 1e4-1e5; a cap of 1000 (set in the reco config) cleanly separates them.

- Remove the temporary domain-count / empty-seed cout diagnostics added during the
  investigation (the empty-seed outsidemap guard itself is retained).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@brownd1978 brownd1978 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this PR. You've clearly understood a lot about how the fit works and how it can fail, and made important improvements.
These fixes make CreateDomains quite long, which suggests it should be refactored. I see big blocks that look very similar: maybe those need to be encapsulated in new functions or lambdas. I wonder if some protection could be moved to BFieldMap. Perhaps BFieldMap could define 'boundary' and '0-field' regions on construction, so that those wouldn't have to be test for by each track. It might help to add a createDomain function that returns a guaranteed usable domain, which could deal with things like minimum field.
Protections connected with sign flipping could maybe be moved to CentralHelix; that should allow omega to change sign while keeping the same particle charge, at least within the same piecetraj.
I suspect the config can be simplified; perhaps a min and max domain range are sufficient. Limits on physical ranges (time) have clearer interpretation than limits on container size.
I believe the minimum field value could go into the BFieldMap constructor: LH and KL won't ever care, so it's fine to set it to a single, global value.
I'm also curious if unusable inputs could be identified and failed before any field calculations, to avoid burdening downstream code with testing.
It's always good to catch failure conditions, but some of the throw/catch blocks seem designed to handle common/routine conditions, which might be clearer to implement as normal functions.
The term 'legacy' in comments won't mean much in a few years: referring to conditions that make the block relevant will be more useful.
I'm happy to discuss if any points above aren't clear.

Comment thread Fit/Config.hh
unsigned maxdomains_ = std::numeric_limits<unsigned>::max(); // hard cap on BField domains per fit; over-cap fits fail cleanly (max = unlimited/legacy)
unsigned minndof_ = 5; // minimum number of DOFs to continue fit
bool bfcorr_ = true; // whether to make BFieldMap corrections in the fit
bool zerofield_extrap_ = false; // if true: (1) bfcorr extrapolate() hands off to geometric free-particle continuation outside the map / below minfield_; (2) createDomains stops DomainWalls at that edge instead of failing Extension; (3) replaceDomains charge/mass mismatch soft-keeps the prior usable fit (CH cosmic CRV). Default false preserves legacy LH/CH behaviour.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would zerofield_extrap_ == true result in different behavior for minfield_ = 0? If not, this config could be replaced with (minfield_ > 0)

Comment thread Fit/Track.hh
if(olditer != last)++olditer;
} while(olditer != last);
}
} catch (...) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what function in the try block is throwing?

Comment thread Fit/Track.hh
// Degenerate active range (or DomainMargin confinement dropping every domain) can leave
// createDomains with zero domains → empty fittraj_. createEffects would then throw
// std::length_error("Empty PiecewiseTrajectory!") and abort the art event. Soft-fail instead.
if(fittraj_->pieces().empty()){

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you test for this failure back at line 337?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants