Repository navigation
Stability controls for the bfcorr BField domain walk - #224
Conversation
…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
left a comment
There was a problem hiding this comment.
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.
…piece, and move B field valid region testing into constructor so that it is precomputed at run start rather than testing for each seed/track.
…r invalid field in CentralHelix ctor.
|
Thanks Dave! Sorry for the delay. I think I've addressed most of your comments and concerns. The one that I deferred is allowing omega to change sign, because that would require significant rewrites in CentralHelix (the derivatives dMdPar would flip sign incorrectly without adding extra logic). The sign flips I saw in testing were associated with a null bnom value passed into the constructor, so I added guards for that instead. |
|
I believe this PR addresses 2 underlying failure cases when extrapolating CH tracks: extrapolating through the rapidly varying field inside the DS coil, and extrapolating poor fits with 'unphysical' interpretation. I believe the intent is to prevent these from causing an (uncaught) exception, which stops event processing, or from causing an 'infinite' loop. I believe the min and max domain sizes address most of these problems. I'd like to better understand the origin of the remaining problems, to see if there's a way to pre-identify them and abort the processing before getting into detailed algorithms. In any event, the new low-level protections are still a good addition. |
New domains should always be created adjacent to existing domains. the low-level (private) createDomains function could be given a range which introduces a domain range gap; that would be a bug in the calling function. |
…and dropped fit) with extendDomains
…ap seeds for field-off (KL) seed conversion.
…of assuming an incompatible piece
|
Hi Dave, thanks for the followup! The createDomains calls that had caused noncontiguous domains were in extend. I replaced that block with single call to extendDomains. However, that did re-introduce another throw-catch to handle the out-of-map case, let me know if you want that part reworked. In A/B testing I found that the production KL output was dropping ~25% of tracks relative to the pre-PR baseline because of out-of-map B field checks, which are not even needed for the KL fit. So I reworked that. The current state of the PR reproduces baseline outputs for all production configs. I also reworded the comments to refer only to current behavior, rather than pre-PR. Lastly, I reworked replaceDomains and extend to remove the assumption about the failure reason from the latter. |
brownd1978
left a comment
There was a problem hiding this comment.
I just realized that many of my comments are still pending, so I'm releasing this review now.
Overall this is great work, thanks for digging out these error cases, I just have a few quibbles.
|
Thanks Rob,
I realized that many of my GitHub comments were pending releasing a review,
a lot of what I said yesterday must have been cryptic! I just released the
review.
As long as the logic in extend for replacing domains when the bfield config
changes remains, updating it to use extendDomains when extending the range
makes sense.
As long as the catch-throws all end up with the fit status correct adding a
layer is fine.
Cheers, Dave
…On Wed, Sep 16, 2026 at 10:03 AM Rob Mina ***@***.***> wrote:
*RobMina* left a comment (KFTrack/KinKal#224)
<#224 (comment)>
Hi Dave, thanks for the followup!
The createDomains calls that had caused noncontiguous domains were in
extend. I replaced that block with single call to extendDomains. However,
that did re-introduce another throw-catch to handle the out-of-map case,
let me know if you want that part reworked.
In A/B testing I found that the production KL output was dropping ~25% of
tracks relative to the pre-PR baseline because of out-of-map B field
checks, which are not even needed for the KL fit. So I reworked that. The
current state of the PR reproduces baseline outputs for all production
configs.
I also reworded the comments to refer only to current behavior, rather
than pre-PR.
Lastly, I reworked replaceDomains and extend to remove the assumption
about the failure reason from the latter.
—
Reply to this email directly, view it on GitHub
<#224?email_source=notifications&email_token=ABAH5754XLB6Y6APDRPDFHT5PLBV5A5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKNZQGEZTONZVGQ42M4TFMFZW63VHMNXW23LFNZ2KKZLWMVXHJLDGN5XXIZLSL5RWY2LDNM#issuecomment-5701377549>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ABAH572XFMQF3LEGEPEOTCD5PLBV5AVCNFSNUABFKJSXA33TNF2G64TZHMZDIMJZHE2TENJYHNEXG43VMU5TKMBQGI3TOMBZGE22C5QC>
.
You are receiving this because you commented.Message ID:
***@***.***>
--
David Brown ***@***.***
Office Phone (510) 486-7261
Lawrence Berkeley National Lab
M/S 50R5008 (50-6026C) Berkeley, CA 94720
|
…n track extension.
|
Hi Dave, thanks for the comments! I pushed a commit that I think addresses all of the concerns. All production configs produce identical output. CentralHelix extrapolates to the CRV with bfcorr=true reliably whereas before it failed on most jobs. |
brownd1978
left a comment
There was a problem hiding this comment.
Apart a minor quibble this now looks very good. Thanks for taking the time to go through my comments.
oksuzian
left a comment
There was a problem hiding this comment.
PR Review Summary — #224
Reviewed at head 134126b540c92a208c7622b8759ab33476ceec24. First pass, second-opinion review after @brownd1978's APPROVED review at the same head; his outstanding request (name the 1.0 in CentralHelix::constructible) is not repeated here.
Decision
- 🟡 comment only. Finding 1 is worth resolving before merge; the rest are minor.
Scope understood
- Bounds the
bfcorrBField domain walk so a degenerate CentralHelix track cannot run the domain count away (OOM), spin on a vanishing step, or escape as an uncaughtstd::invalid_argument/std::length_error. - Two new
Configknobs (mindtstep_,domainmargin_), both no-ops at their defaults; low-field protection moved into theBFieldMapconstructor (minfield_), soprotecting()is off unless the experiment passes it. - New soft-fail paths (
Status::unphysical,validInput,hasTraj) plus a two-phasereplaceDomains.
Findings
-
🟠 [S1]
extendDomainswas left out of the hardening, and one of the new throws now escapes through it- Evidence:
Fit/Track.hh:745-781. The two domains it builds sample the field with a barebfield_.fieldVect(straj.position3(range.mid()))(lines 762 and 778) and take the step asstd::max(rangeInTolerance(...), mindtstep_)with no positivity guard.createDomain(line 845) guards both cases —bfield_.usable()at 851,usableField()at 865, andif(dhi <= tstart) return std::nulloptat 853 — andupdateDomains(line 740) keeps the previous field when the sample is unusable, soextendDomainsis the one domain-producing path with neither guard. Its two call sites,iterate()at line 573 andprocessEnds()at line 793, are not inside atry, unlike the newtryinextend().- Null BNom:
Domainwithbnom = (0,0,0)reachesaddDomain→DomainWall::append→newpiece.resetBNom(next_->bnom())(Fit/DomainWall.hh:109), which now throwsstd::invalid_argument("CentralHelix::resetBNom; null BNom")(Trajectory/CentralHelix.cc:93-95). Nothing between there and the caller catches it, so it leavesTrack::fit()— the uncaught-throw failure mode #4 that this PR removes elsewhere, recreated by the new guard on a path that does not pre-check.initTrajhas the same shape at line 628 (front.resetBNom(domain->bnom())), fed by domains thatcreateDomainbuilds from the rawfieldVectfallback at line 867 wheneverprotecting()is false. - Reachability in Mu2e:
KKBField::fieldVectreturns exactly(0,0,0)for a point outside all maps (Offline/Mu2eKinKal/src/KKBField.cc:14-19), andKKBField::inRangetests only the inner maps' bounding boxes, soinRangecan be true where the lookup fails.rangeInTolerancereturns the whole piece length whend2pdt2 <= 1e-10(General/BFieldMap.hh), sorange.mid()can sit far outside the map even whentimeis inside it. OnlyCentralHelixhas the new guard —LoopHelix::resetBNom(Trajectory/LoopHelix.cc:84) andKinematicLinedo not — which is consistent with the identical LoopHelix/KinematicLine A/B output, and leaves the exposure on the CentralHelix cosmic path this PR targets. - Non-advancing step:
rangeInTolerancereturnssqrt(dp/d2pdt2)withdp = momentum(tstart)*tol, and the collapsing momentum described in failure mode #1 drives that to zero. Withmindtstep_at its default0.0,dt == 0givesTimeRange(time,time), which is a legal null range (General/TimeRange.hh:14-31),Domain::operator<dedupes it in thestd::set, andtime = domain.begin()never advances — failure mode #2, in the functionextend()now routes through.
- Null BNom:
- Impact: an event aborted by an uncaught exception, or a CPU spin, in the configuration Offline runs today (
minfield_unset, soprotecting()is false andmindtstep_is0.0). - Suggested fix: build these two domains with
createDomainas well, soextendDomainsinherits the field check and the guaranteed-positive step; failing that, replicate both guards and decide whatextendDomainsshould return when it cannot advance.
- Evidence:
-
🟡 [S2]
std::clampwith an unordered pair of bounds- Evidence:
Fit/Track.hh:966and1005,std::clamp(rangeInTolerance(...), config().mindtstep_, xtest.maxDtStep()).std::clamprequires!(hi < lo); nothing relatesmindtstep_(a KinKalConfigvalue) tomaxDtStep()(from the experiment's extrapolation predicate — Mu2e sets 1.0 ns for three of four and 10.0 ns for one,Offline/Mu2eKinKal/fcl/prolog.fcl:377,390,403,416). Anymindtstep_above 1.0 ns — a plausible value for the failure mode #1 fix — is undefined behavior; libstdc++ returnslo, silently exceeding the extrapolation's own maximum step. The pre-PR line wasstd::min(rangeInTolerance(...), xtest.maxDtStep()), which could not violate a precondition. - Suggested fix: apply the two bounds in a defined order (
std::max(std::min(dt, maxDtStep), std::min(mindtstep_, maxDtStep))), or validatemindtstep_ <= maxDtStep()once and say which bound wins.
- Evidence:
-
🟡 [S2] The two
extrapolatebranches report a caught exception differently- Evidence: in the protecting branch the catch at
Fit/Track.hh:982leavesretval = false; in the unprotected branch the identical catch at line 1016 is immediately overwritten byretval = trueat line 1018. That line predates the PR, but the restructuring is what makes the two configurations disagree, and the return value is live in Offline:KKExtrap.hh:156doesif(!ktrk.extrapolate(tdir,toPerim)) return;, so once Offline passes aminfieldthe same out-of-map exception will abandon the rest of the extrapolation chain where today it continues. - Suggested fix: drop line 1018, or comment why a caught out-of-map exception is a success only for the unprotected walk.
- Evidence: in the protecting branch the catch at
-
🟡 [S2] Non-finite momentum stops the extrapolation with no trace
- Evidence:
Fit/Track.hh:956,if( !std::isfinite(ktraj.momentum(time)) ) break;— nohanded_off, nohistory_entry, and no geometric continuation, so the fit staysusable()with a silently truncated extrapolation. Every other early exit in that branch records something. This is the same objection you raised on earlier revisions atTrack.hh:324and:377. - Suggested fix: record a
Statusbefore breaking, or sethanded_offso the existing handoff bookkeeping covers it.
- Evidence:
-
⚪ [S3] Two small items
- The PR description still documents
minfield_,maxdomains_andzerofield_extrap_as newConfigknobs; the final diff adds onlymindtstep_anddomainmargin_, withminfield_moved to theBFieldMapconstructor and the other two dropped. Worth refreshing before merge, since the body is the merge record. ParticleTrajectory::compatiblereturns true on an empty piece list, soprependto an empty trajectory no longer throws frommass()(Trajectory/ParticleTrajectory.hh:44-45, "this will throw for empty"). That looks like an improvement, but it is a contract change forprependthat the PR does not mention.
- The PR description still documents
Verified
- 🟢
CentralHelix::constructiblefaithfully mirrors the constructor it stands in for. The rotation matchessetTransforms()(CentralHelix.cc:134) term for term, andmomToRad,radius,amsign,phirmandlcentmatchCentralHelix.cc:48-63; the two refusals correspond to the constructor's own throws — null BNom at line 39 andrcent < 1.0at line 65. TheParticleStateconstructor delegates to the pos/mom constructor (line 114), so the predicate covers the constructor actually reached. It is therefore not stricter than the constructor and cannot refuse a seed that previously fitted. The duplication and the unnamed1.0remain as you noted. - 🟢
Status::unphysicalis appended last, so no existing enumerator changes value;usable()(status_ > unfit && status_ < lowNDOF) classifies it unusable, and thestatusNameswitch covers it. Mu2e does not persist the KinKal enum —KKFit.hh:687,699maps it intoTrkFitFlag— so there is no backward-compatibility question for existing files. - 🟢
replaceDomainsis behavior-preserving on the success path: the copy is built and committed beforedomains_/effects_are touched, and the ordering ofupdateReferenceand the traj swap is unchanged. The added guard matches thesetRangefailure it describes. It does cost one deep copy of the trajectory per call; that is on the domain-replacement path only, not per iteration, so it is unlikely to matter.
Validation check
- Build/tests run: no. Nothing was compiled or run for this review; the PR's own A/B evidence (identical output for LoopHelix, CentralHelix cosmics and KalSeed domain bounds; 3.2 GB/43.5 s → 1.0 GB/4.0 s on event 5160) was read, not reproduced.
- Config contract check: partial. The new knobs are no-ops at their defaults, and
protecting()is off until an experiment passesminfieldto theBFieldMapconstructor. - Cross-repo consistency: needs follow-up. No open Mu2e PR sets
minfield,mindtstep_ordomainmargin_, so in Offline as it stands the low-field handoff branch is unreachable and the unprotected paths — including the one in finding 1 — are the ones that run.
Residual risk
- Finding 1 is a code-path argument, not a reproduced crash; it needs a domain whose midpoint samples a null field, or a zero step, on the
extendDomainspath. - The
bfcorr=trueCentralHelix→CRV configuration this PR is for is not exercised by the A/B samples, which ran production configs with the knobs off.
Author follow-ups
- Give
extendDomainsthe same field and positive-step guards ascreateDomain, ideally by calling it (finding 1). - Make the
std::clampbounds well-ordered, or validatemindtstep_ <= maxDtStep()(finding 2). - Decide whether a caught extrapolation exception should fail the extrapolation in both branches (finding 3), and record the non-finite-momentum stop in
history_(finding 4). - Refresh the PR description to the knobs that survived, and say which Offline PR will pass
minfield(finding 5).
…nfiguration, record non-finite momentum failure, correct return value when extrapolation exception caught, extendDomains calls createDomain.
8f86ee8 to
2ee0f64
Compare
|
Thanks for the review! I agree with all the suggestions. All are now implemented. |
brownd1978
left a comment
There was a problem hiding this comment.
Thanks for all your work on this Rob!
Summary
Bringing up
bfcorr=trueCentralHelix→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/ω; thedPardB ~ bfrac/(1+bfrac)correction has a pole atbfrac = −1). Near-vertical cosmics leaving the DS bore justpast the tracker drive the walk straight into that region.
The failure modes
rangeInTolerancereturns a vanishingdt, andaddDomainappends an effect + traj piece per step without bound.BCorrTolerancedrivescreateDomains'trange→0, sotstartstops advancing and the loop spins emitting deduped domains into astd::set.throw std::invalid_argument("Invalid domains")extendbuilds extension domains in independentcreateDomainscalls, each with its own ±0.5·trangemargin; the blocks don't tile contiguously andcreateEffectsrequires abutting domains.throw std::length_error("Empty PiecewiseTrajectory!")New knobs
Config::mindtstep_0.0nsmax(rangeInTolerance, 0) == rangeInTolerancerangeInTolerance→0. Must not exceed the extrapolation'smaxDtStep()(checked, throws)Config::domainmargin_DBL_MAXnsBFieldMap(double minfield)0.0T|B|≥minfield; extrapolation below it hands off to geometric free-particle continuation, before thedPardBpoleAlways-on changes
These are not behind a knob. Each only fires where the previous code had undefined behavior or an
uncaught throw, so a well-behaved track is unaffected:
extendgrows existing domains withextendDomainsinstead of independentcreateDomainscalls, so extension domains always abut (Update2 #3).detectorRangereturns a nullTimeRangewhen there are no active hits/xings, instead of an inverted range that throws.replaceDomainssoft-fail with a status (no domains, domains not spanning the trajectory, empty trajectory — Mat #4) instead of throwing or dereferencing an empty container.replaceDomainsbuilds the replacement before touchingdomains_/effects_, so a failure leaves the track intact.Status::unphysical, viaParticleTrajectory::compatible(), instead of throwing out ofappend.CentralHelix::constructible()(trivially true for LoopHelix/KinematicLine) refuses a null field or a circle centered on the axis before construction;resetBNomrefuses a null BNom. Domains are never built with a null BNom.Verification
No effect when unconfigured. Mu2e Offline built twice on the same source base, differing only by this
branch versus its base
2011f34, identical input, no knob set, compared exactly (rtol = 0):OnSpillreco)Stability. Event 5160 under a 4 GB address-space cap, knobs off versus on, with nothing else changed:
FatalRootErrorfinalizing a 367,596-domain runawayWith the knobs set (
mindtstep_0.03 ns,domainmargin_0,minfield1e-6 T),bfcorr=trueCentralHelix→CRVreco runs over 112 Run-2 cosmic files (~98k events) with no crashes, hangs or domain errors.
The Mu2e Offline plumbing for these knobs will follow in a separate Offline PR.