Skip to content

Port CVH refit + WMass NanoAOD customizations to CMSSW_15_0_19_patch2#46

Draft
davidwalter2 wants to merge 178 commits into
WMass:WmassNanoProd_15_0_19_patch2from
davidwalter2:WmassNanoProd_15_0_19_patch2_dev
Draft

Port CVH refit + WMass NanoAOD customizations to CMSSW_15_0_19_patch2#46
davidwalter2 wants to merge 178 commits into
WMass:WmassNanoProd_15_0_19_patch2from
davidwalter2:WmassNanoProd_15_0_19_patch2_dev

Conversation

@davidwalter2

@davidwalter2 davidwalter2 commented May 13, 2026

Copy link
Copy Markdown

Summary

Port of the CVH (Continuous Variable Helix) track-refit infrastructure and the WMass custom-NanoAOD producers from WmassNanoProd_10_6_26 to CMSSW_15_0_19_patch2 (el9_amd64_gcc12, Geant4 11.x), plus the development layered on top since the initial port: multi-threaded CVH, the ScalarPot3D spherical-harmonic field model with 5×9 transport Jacobians, the sparse-GBL two-track fit, and the new B±→J/ψ(→μμ)K± calibration chain.

146 commits — 104 Josh Bendavid (original CVH development, cherry-picked chronologically with authorship preserved), 31 David Walter (port glue + field-model/threading/validation work), 6 Pietro Lugato (B→J/ψK chain), 2 Davide Bruschini (ForCVH Geant4-11.x physics classes), 2 Elisabetta Manca, 1 Kenneth Long.
131 files, +23.8k/−0.1k lines. Full tree builds clean with scram b -j8.

1. Base port

  • 105 cherry-picks from WmassNanoProd_10_6_26: the CVH residual-maker plugins (ResidualGlobalCorrectionMakerBase/G4e/TwoTrackG4e in Analysis/HitAnalyzer), the customized Geant4ePropagator (double-precision propagateGenericWithJacobianAltD with per-step MS/ionization error decomposition, Urban-model variance with α=0.999 truncation), value-map producers, and per-channel configs.
  • Bruschini's *ForCVH physics stack (from WMassRefits_CMSSW_15_1_X): physics list (no MSC, no secondaries), energy-loss tables, Universal Fluctuation model, ErrorEnergyLoss, ErrorMessenger — adapted to G4 11.x thread-local G4ErrorPropagatorManager/G4ErrorPropagatorData (all G4e init deferred to first propagate per thread, mutex + thread-local guarded).
  • 15_0 API ports: FlatTable addColumn type inference, ESHandle → ESGetToken, SiStripClusterInfo per-producer construction, Eigen::placeholders::all, dd4hep-default geometry kept.

2. Post-port development

  • Multi-threaded CVH (CvhMasterThread/CvhMaster/CvhWorker, SimG4Core OscarMTMasterThread-style master/worker): makers are now edm::stream::EDProducer<edm::GlobalCache<CvhMasterThread>> with idempotent per-TBB-thread worker setup. One G4 master per module instance → one maker per cmsRun job (drives the per-leg job design below). Replaces the -j1 GeometryProducer path (kept for legacy tests).
  • ScalarPot3D field model (MagneticField/ParametrizedEngine): C++ port of the mfs spherical-harmonic scalar-potential basis (Φ = Σ c_{l,m}(R/s)^l P_l^m(cosθ){cos,sin}(mφ), B = ∇Φ), loaded from a coefficient dump, with per-mode basis getters for calibration-fit Jacobians. Thread-safe by construction (immutable state, stack-local per-call cache — 7.5× faster than the naive evaluation) — this replaces the thread-unsafe PolyFit3D, which stays hard-disabled. Element-wise closure vs mfs at 1e-12; validity sphere R ≤ 320 cm. Published under IdealMagneticFieldRecord label ScalarPot3DMf.
  • 5×9 transport Jacobian: dBx/dBy columns added to the propagator Jacobian (10_6 had only dBz); SymPy-generated body with standalone finite-difference closure (exact dBz equivalence with the old generator, 1e-7 FD closure on dBx/dBy). sim::Field gains per-propagation additive field/material offsets so the G4 stepper and the analytic Jacobian see the same B+dB (no double counting; default path behavior-neutral).
  • Scalar-potential B-field correction in the makers: ~50 global harmonic modes (parmtype 14) replace the ~15k per-module ΔBz parameters; per-hit per-mode (Bx,By,Bz) basis values chain-ruled against the 5×9 Jacobian, with optional runtime FD closure (runFDClosure).
  • MagneticFieldLabel routing (opt-in, default "" = standard behavior): SiPixel/SiStrip CPE ESProducers, TransientTrackBuilder, TrackRefitter, GeometryProducer, GeantPropagatorESProducer can all consume a labelled field, so the CVH chain runs on ScalarPot3D (or Opera3D grid via useOpera3D) without touching default reco.
  • Sparse GBL formulation of the two-track fit: sparse residual/Jacobian assembly + SimplicialLDLT solve replaces dense Hessian assembly — ~3.3× less memory, validated bit-identical.
  • Candidate-driven two-track path: optional srcCandidates (VertexCompositeCandidates) input — one output row per persisted Stage-1 candidate; per-particle Geant4e propagation for V0 daughters (π/K/p mass hypotheses via a single PDG-table source of truth); V0CandidateProducer removed.
  • Robustness: fillGrads SIGSEGV fixed (TTree::SetAutoSave(0)), TTree memory bounded, per-stream output files.
  • NanoAOD: FlattenedCandValueMapVectorTableProducer flattens per-muon ValueMap<vector<int/float>> (CVH global indices + Jacobians) into FlatTables; eight scalar CVH ExtVars wired into muonTable.

3. B±→J/ψ(→μμ)K± chain (new)

  • JpsiKCandidateSplitter: splits Stage-1 B⁺ candidates into a dimuon collection + bachelor-kaon TrackCollection, with parallel B-candidate-index vectors and per-leg pdgId vectors for multi-channel mass-hypothesis handling; bCandIdx is written to both makers' trees for offline (run, lumi, event, bCandIdx) joining of the two legs.
  • Two new channel cfis: TwoTrackJpsiKMuMuG4e (J/ψ leg, mass constraint on) and JpsiKSingleTrackKaonG4e (bachelor leg, kaon hypothesis).
  • Driver runCvhBplusJpsiK.py: one maker per job (mode=dimuon|kaon), ScalarPot3D as base field (with fallback A/B switch), kaonAsMuon mass-hypothesis knob, and convergence-study instrumentation (configurable GN iterations/EDM threshold, midPropagated starting state, per-iteration debug branches).
  • "Double B field" fix: seeding parmtype-14 corparms_ with absolute scalar-potential coefficients while the same field was already the process base field made getCorrectionAt() re-add the full base field as dB (doubling B, halving refit q/p). Fixed by starting corrections at zero — corrections are now deltas on the base field. Open FIXME: parmtype-14 entries in corFiles are currently skipped on load, so fitted B-field corrections cannot yet be re-applied in a refit iteration.
  • Removes the stale duplicate residual-maker copies under RecoTracker/TrackProducer/plugins/.

Intentionally excluded

  • PolyFit3D (thread-unsafe, removed upstream long ago) — superseded in this PR by ScalarPot3D.
  • Fixed backward propagator cms-sw/cmssw#32833 backport (native in 15_0) and the 10_6 dd4hep removal (contrary to 15_0 defaults).
  • Non-CVH 10_6 NanoAOD customizations (gen-weight/LHE/Mu T&P) — deferred; 15_0 NanoAOD is structurally different.

Validation status

  • scram b -j8 clean (all packages, plugins refreshed)
  • ScalarPot3D basis closure vs mfs reference at 1e-12 (element-wise, standalone test)
  • 5×9 Jacobian FD closure (λ ~1e-6, φ at the forward-FD limit ~1e-3–1e-2, q/p conserved); per-mode chain-rule FD closure in the maker
  • CVH J/ψ refit smoke test on 48-event 15_0-native ALCARECO: 47/48 J/ψ candidates, m(J/ψ) = 3.066 GeV, closure at FP-noise level. (Cross-release 10_6 ALCARECO inputs are known-bad in 15_X — SiStripCluster deserialization bug, ROOT Replace boost with standard library equivalents cms-sw/cmssw#19773 — use 15_0-native inputs.)
  • Post-review-fix smoke: two-track J/ψ refit (runCvhJpsi.py, ScalarPot3D field) on 50-event repacked ALCARECO with 2 threads/2 streams: ~6000 propagations, 0 failures (0 ierr, 0 path-length aborts), mean m(μμ) = 3.090 GeV. Exercises the reworked G4 lifecycle, per-stream propagators, and the particle-set registry. The single-track kaon leg and the hadron fluctuation-table rebind are not yet exercised at runtime (needs a B→J/ψK input).
  • B→J/ψK legs on ALCARECO validation sample (in progress — this is what the new driver is for)
  • NanoAOD branch parity vs the 10_6 reference
  • Single-job CRAB test

Review findings and their resolution

A full-diff review (July 2026) found the issues below. All high/medium items are fixed on the branch (commits cc97b5107c1, 631b221040e) unless noted:

Fixed (was: must fix):

  1. Geant4ePropagator fluct segfault with nStreams > nThreads — per-instance allocation hoisted out of the per-thread init guard; validated with a 2-stream smoke run.
  2. Duplicate DEFINE_FWK_MODULE names — the Analysis/HitAnalyzer copies of GlobalIdxProducer/TrackProducerFromPatMuons/TrackExtraRekeyer were removed; the canonical (global, fillDescriptions, extended) versions live in RecoTracker/TrackProducer.
  3. muons_cff.py dangling trackrefit/trackrefitbs references breaking standard NanoAOD — CVH muon branches moved behind an explicit nanoAOD_addCvhMuonBranches(process) customization that validates the refit modules exist.
  4. SiPixelTemplateDBObjectESProducer breaking bare HLT-menu instantiationsfillDescriptions with MagneticFieldLabel default "" added; GeometryProducer tolerates an absent parameter.

Fixed (was: should fix):
5. Multi-run G4 teardown — the G4 world is now an explicitly job-scoped resource: CvhMasterThread::ensureG4Started() starts it exactly once at the first globalBeginRun, it survives run boundaries, and teardown happens in stopThread() at end of job. Since the world captures the run-1 geometry/field ES products, a mid-job IOV change (e.g. a magnet-current change via AutoMagneticFieldESProducer) is detected via the ES cache identifiers and refused with a clear cms::Exception instead of silently refitting with stale conditions (b5940c1ee84).
6. Hadron ionization variance using the muon dE/dx tableSampleFluctuations/SampleFluctuations2 now rebind particle + table via SetParticleAndCharge (proton-table scaling for hadrons) before the dE/dx lookup, consistent with the mean-loss model. lastMaterial initialized.
7. Physics-list particle narrowing vs full-list re-instantiation — job-wide particle-set registry in G4ErrorPhysicsListForCVH: the propagator's no-arg instances reproduce the set configured via CvhMaster.Particles, so no process-less particle definitions can appear on worker threads.
8. Max path length 200 cm → 100 m: intentional, kept, now documented in-code — low-pT curling tracks (B→J/ψK bachelor kaons, helix radius ~50 cm) legitimately accumulate >2 m of path before intersecting the target surface. Note this is a deliberate loosening relative to both stock CMSSW and the 10_6 fork (both 200 cm).
9. Debug std::cout in sim::Field constructor — removed.
10. ScalarPot3DMagneticField missing setNominalValue() — added (nominal 3.8 T now cached); out-of-sphere queries emit a LogWarning instead of silently returning B=0. The C¹ blend to the volume-based field outside the sphere remains future work.

Low-severity items — swept in 2a32331fe10: dead G4WentzelVIModelForCVH removed; -g CXXFLAGS, unreferenced files/deps, vestigial counters, and stale comments/help text cleaned up; kaon q/p diagnostic gated on the B→J/ψK path; JpsiK cfi useIdealGeometry defaults fixed; FlattenedValueMapVectorTableProducer single-type null-deref guarded; ScalarPot3DEval header validation hardened; EmptyESSource moved from the ScalarPot3D cfi to the standalone test cfg. Duplications consolidated by checking out the owning package (RecoTracker/TrackProducer): single home for the ValueMap<vector<int>> dictionary and the MagneticFieldOffset/OffsetMagneticField helpers, and its BuildFiles reset to stock + the deps the WMass additions actually need (fixes the missing DataFormats/PatCandidates for the pat::Muon dictionaries and removes 13 orphaned deps of the deleted residual-maker copies).

Deliberately kept: dead computeLandau/SampleFluctuations2 + always-zero landauDelta/landauW tuple slots (10_6 parity; removal is API churn across both makers — separate commit if wanted); HitAnalyzer's MatRepStd SMatrix dictionary (unused by current code but possibly needed to read older trees); channel cfis remain driver-injected for scalarPotentialInitFile/CvhMaster (by design).

Remaining work

  • B→J/ψK validation (kaon leg exercises the hadron fluctuation-table rebind at runtime); then the combined NMR + J/ψ + B→J/ψK scalar-potential calibration fit
  • Delta-basis definition for parmtype-14 so fitted B-field corrections can be loaded in refit iterations
  • C¹ boundary blend of ScalarPot3D to the volume-based field outside the fit sphere
  • NanoAOD branch parity + CRAB test

🤖 Generated with Claude Code

bendavid and others added 30 commits May 13, 2026 17:04
… info for wedge modules, restore ability to apply linearized corrections to two track output, and additional disabled debugging
… hits), exclude loopers from two track fit, add muon matching and id info to two track fit
davidwalter2 and others added 30 commits July 14, 2026 20:08
…tion

The job-scoped G4 world captures the geometry and magnetic-field ES
products at first start and cannot be rebuilt. Record the ES cache
identifiers and throw cms::Exception('Conditions') if either record's
IOV changed at a later run boundary (e.g. AutoMagneticFieldESProducer
picking a different payload after a magnet-current change), instead of
silently propagating with stale conditions.

Co-Authored-By: Claude Fable 5 <[email protected]>
…kout

Dead code / leftovers:
- Remove G4WentzelVIModelForCVH (compiled but never instantiated; 10_6 used
  it only inside if(false) blocks that the port already dropped)
- Remove unreferenced SimG4Core/MagneticField/FieldParameters.xml and
  FWCore/PluginManager dep; drop -g CXXFLAGS from Geant4e BuildFile
- Remove vestigial nValid(Pixel)HitsFinal counters (incremented but never
  read; were uninitialized in two-track jobs)
- runCvhBplusJpsiK.py: delete dead _resolve_default_corfile(), correct the
  corFiles-default comment, update stale midPropagated help text, reject
  mode=both at config time instead of documenting the G4-singleton crash
- Fix swapped FWD/BCKD verbose labels in G4ErrorEnergyLossForCVH; update
  stale species-disambiguation comment in JpsiKCandidateSplitter cfi

Small fixes:
- Kaon q/p diagnostic now gated on the B->J/psiK path (bCandIdxSrc set)
  instead of particle name, so plain muon single-track jobs stay quiet;
  atomic gate can no longer overshoot under concurrency
- JpsiK channel cfis: useIdealGeometry defaults False (True + empty
  corFiles is the broken Stage-1-only combination the maker warns about)
- FlattenedValueMapVectorTableProducer: always put all declared tables
  (empty when a type has no configured variables) instead of null-deref
- ScalarPot3DEval: throw on missing r_scale header (was silently 1 cm)
  and on negative l_max
- ScalarPot3D cfi no longer ships an EmptyESSource (conflicts with the
  standard field's record provider when process.load'ed); the standalone
  test cfg declares its own

Consolidation (add the owning package instead of duplicating):
- RecoTracker/TrackProducer added to the checkout; its BuildFiles reset to
  stock + deps actually needed by the WMass additions (PatCandidates for
  the pat::Muon dictionaries, MuonReco, VolumeBasedEngine); removes the 13
  orphaned deps of the deleted residual-maker copies, -g, and the illegal
  plugin-library export
- edm::ValueMap<vector<int>> dictionary now declared only in
  RecoTracker/TrackProducer; HitAnalyzer's duplicate (and the
  vector<float> one already provided by DataFormats/Common) removed
- Delete HitAnalyzer's unused copies of MagneticFieldOffset /
  OffsetMagneticField and the dead includes referencing them

Validated: scram b clean; 50-evt 2-stream J/psi CVH smoke, ~6000
propagations, 0 failures, mean m(mumu)=3.090 GeV.

Co-Authored-By: Claude Fable 5 <[email protected]>
…ug trace

Traced the rare CVH refit failures (0.58% of J/psi ditrack candidates,
0.025% of single-track fits, 10k-event Run2016F ALCARECO) to runaway
propagation legs: with PropagationDirection=alongMomentum every hit-to-hit
leg is forced forward onto an INFINITE target plane, so a plane marginally
behind/tangent to the state (kinfit seed displaced from the Kalman
trajectory; worst at stereo-partner surfaces) sends the track out of the
ScalarPot3D 320 cm validity sphere where B=0 -> straight line -> it either
grinds to the G4 extrapolator table floor (Ekin=1 MeV, p=14.6 MeV) or gets
pinned at the G4 world boundary (r~17.5 m) -- and both were returned as
propagation *successes*, poisoning the sparse solve with NaN.

Geant4ePropagator (propagateGenericWithJacobianAltD):
- in-flight momentum floor (exit4[pdrain]): fail a leg once |p| < plimit
  instead of returning a drained garbage state
- on-surface closure check (exit5[offsurface]): final position must lie
  within 1 mm of the destination plane (kills world-edge zombie legs and
  their zero-step "successes")
- backward-mode momentum flip-back: the AltD variant was missing the
  flip-back the TSOS variant has, so anyDirection returned reversed
  momentum. With this fixed, propagationDirection=anyDirection recovers
  11/11 previously-failing candidates (9/11 genuine J/psi on the peak,
  normal 3-iteration convergence); kept opt-in pending large-scale
  validation of backward-leg (flipped-frame) transport Jacobians.

ResidualGlobalCorrectionMaker{G4e,TwoTrackG4e}:
- NaN fail-fast right after the sparse solve; previously NaN leaked into
  the charge-sum check (copysign(1., nan)) and was miscounted as a charge
  flip
- skip same-sign pairs before the kinematic fit (all two-track channels
  fit a neutral parent; 0.41% of ALCARECO pairs are same-sign and aborted
  deterministically after wasting a kinfit + one GN iteration)
- per-job fit summary counters (attempted/succeeded/fail-by-category)
  printed from the destructor; abort prints carry seed kinematics and
  iteration context
- dbgSeed/dbgIter/dbgHit stdout trace behind debugPerIterDump (survives
  candidates whose fit aborts, unlike the tree branches)

Drivers:
- runCvhSingleTrack.py: catch up with the CvhMaster GlobalCache port
  (CvhMaster PSet + per-stream RNG service, geopro dropped from the path);
  the driver could not construct the producer at all since that refactor
- runCvhJpsi.py: new knobs debugPerIterDump, eventsToProcess,
  propagationDirection

Validated on 10k events: succeeded fits and physics bit-consistent with
baseline (9784 entries, m=3.08933+-0.05307 in both); the 57 baseline
losses become 40 same-sign skips + 17 fast, correctly-labelled
propagation failures.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EXjsuBb3HHCTK8L1jBx75j
…gle-track GN step floor; multi-run field IOV fix

Follow-up to ba2b3cf, validated on 100k Run2016F J/psi events.

Geant4ePropagator:
- propagateGenericWithJacobianAltD: exact frame conversion of all
  derivative/noise outputs for backward legs. They are accumulated on the
  momentum-flipped trajectory; under momentum reversal the CMSSW
  curvilinear basis transforms with P = diag(1,-1,1,-1,1) (q/p even,
  lambda odd, dphi even, xT odd, yT even), so return P*J*P for the state
  block, P*cols for the dB/dxi columns, P*Q*P for the noise matrices, and
  -dEdx (the retraced path gains energy where the physical track loses
  it). Per-leg gradients and Hessians are now exact for backward legs of
  any length. `backwardLegs` counter added to the job summary.
- Geant4ePropagator_cfi: PropagationDirection default alongMomentum ->
  anyDirection. A/B on 100k events: every fit that succeeds with
  forward-only propagation is bit-identical under anyDirection (97,773
  dimuon + 196,957 single-track candidates, zero exceptions); strict
  superset with +218 dimuon / +28 single-track recovered candidates and
  none lost. Failure rates: dimuon 0.263% -> 0.041%, single-track
  0.033% -> 0.019%. With the frame conversion the recovered candidates'
  masses move by <= 0.44 MeV (median 0.05 MeV); a 2k slice against the
  pre-conversion run is bit-identical except exactly the recovered
  candidates.

ResidualGlobalCorrectionMakerG4e (single-track):
- momentum-floor safeguard on the Gauss-Newton step: scale the step only
  when the q/p update would push the reference momentum below 2 GeV or
  flip its charge. This eliminates the divergence where one unphysical
  update dragged p from 3.6 GeV to 0.54 GeV and the next propagation was
  refused: 100k validation shows 0 such failures in 197,022 tracks (was
  1/4k), clamp active in 261 fits (0.13%), first-2k bit-compare against
  the pre-clamp reference 3923/3927 identical -- the 4 changed fits were
  silent divergences (edm 1e6..4e8) that now end finite, and the
  originally-failing track is recovered. A generic relative-step cap was
  deliberately NOT used: forward tracks legitimately repull q/p by O(100%)
  in early iterations. `clamped[step]` counter in the fit summary.

Multi-run jobs (needed to run past ~30k events of a 2016F file):
- ParametrizedMagneticFieldProducer: cache the field and return the same
  shared object for every IOV. The parametrized field is a pure function
  of the configuration; IdealMagneticFieldRecord rolls every run through
  its RunInfoRcd dependency and previously rebuilt an identical product,
  which the CVH G4 world (holding the raw pointer job-long) could not
  tolerate. The volume-based (useOpera3D) path still rebuilds per run.
- CvhMasterThread: on a run-boundary cacheIdentifier change, re-fetch the
  field product and accept the boundary iff it is the same object the G4
  world was built with; only a genuinely rebuilt product throws.
  Validated across runs 278769 -> 278808.

Drivers: propagationDirection option defaults to anyDirection (both
drivers); runCvhSingleTrack.py gains numberOfThreads.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EXjsuBb3HHCTK8L1jBx75j
The per-candidate global-parameter Hessian H = 2 J^T R J has rank
rank(Vinv) - nstatefree ~ ndof (+1 mass row), i.e. 16-45 for J/psi
candidates, while hesspackedv stores the dense nParms(nParms+1)/2
triangle (~570 kB/candidate at 360 scalar-potential modes, ~97% of the
grads output; ~50 TB extrapolated to 2016 F+G+H).

New untracked options fillGradsFactored (default False) and
hessFactorTol (default 1e-8, the fidelity floor of the float32 packed
storage): eigendecompose hess in double precision at the fill site,
keep modes above tol*lambda_max, and store B (nRank x nParms row-major,
rows sqrt(lambda) v^T by decreasing lambda, convention H = B^T B) in
branches nRank/nFactor/hessfactorv plus the truncation monitor
hessdroppedmass. gradv is unchanged and now also written when only the
factored storage is enabled; hesspackedv stays available via fillGrads
for A/B validation.

Validated on the 50-evt ALCARECO smoke (both storages on, mass
constraint, lmax18 coefficients): nRank = ndof..ndof+3, dropped
eigenvalue mass ~1e-10, per-candidate and accumulated-global-Hessian
reconstruction at the float32 floor (~1e-7), 566 -> 60 kB/candidate
(9.4x). Downstream accumulation becomes a syrk:
hess[gi,gj] += sum_k B[k,i] B[k,j].

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UvHBa3aKnPnkKZ7GHRDQLv
beginRun declared a local const double dtheta, shadowing the member the
runtree branch points to, so the branch held stack garbage (~1e-310
denormals) for all module rows -- and made runtree differ spuriously
between output files. Assign the member instead, clamping the dot
product to [-1, 1] so acos cannot return NaN for numerically parallel
axes.

Verified on the ALCARECO smoke: no NaNs/denormals, field-mode rows stay
0, median aligned-vs-ideal local-x rotation 1.9 mrad (99% < 15 mrad,
plus a genuine large-rotation tail of 132 pixel modules), and runtree
is now bit-identical across stream output files.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UvHBa3aKnPnkKZ7GHRDQLv
…egion

All tracker modules lie strictly inside the ScalarPot3D validity sphere
(outermost module corners at R ~ 295 cm vs. R = 320 cm), so a state
outside it is unambiguous proof of a runaway/wrong-way leg. Check
MagneticField::isDefined() after every G4 step in
propagateGenericWithJacobianAltD and fail the leg within one step
(<= 10 mm) of leaving -- metres earlier and thousands of steps cheaper
than the momentum-drain / off-surface backstops, and long before the
B=0 fallback region can silently straighten the trajectory. For field
models defined everywhere (volume-based map) the guard never fires.

New exit6[fieldbound] counter + per-failure kinematics print. Validated:
with forward-only propagation forced, all 11 known runaway candidates
are caught exactly at the sphere crossing (p still 3.2-17 GeV,
pathLen 2.4-3.1 m, vs. draining to the 1 MeV table floor after 3.5-7 m
or 18+ m world-edge flights before); 50-event smoke with the
anyDirection default shows zero guard activations and unchanged fits.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EXjsuBb3HHCTK8L1jBx75j
…, in-loop chi2 fix

- nIters / edmConvergence configurables (existsAs-guarded, defaults 10 /
  1e-5 reproduce the baseline exactly), exposed in both test drivers.
- Optional Gauss-Newton step damping (gnDampAfter/gnDampFactor, default
  off): remedy for limit cycles between chi2-degenerate states observed
  in ~8% of single-track fits (at-cap fraction 16% -> 4.4% with
  gnDampAfter=6); a half-step from a 2-cycle endpoint lands on the
  midpoint.
- debugPerIterDump: per-iteration chisq/EDM trajectory vector branches,
  mirroring the two-track maker; edmvalref_iter (the actual convergence
  criterion) added to the two-track dump as well.
- Fix: chisqval/deltachisqval were never filled inside the iteration
  loop (dead members) -- now track the realized chi2 at the current
  linearization point plus the predicted step change, as in the
  two-track maker. The final stored chisqval is still overwritten with
  the residual-projector value after the loop, as before.

Validated bit-identical (7856/7856 fits) against the pre-change
reference at default settings.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EXjsuBb3HHCTK8L1jBx75j
… modules

Modules that were off during data-taking collect no hits, so their
alignment is unconstrained and the persisted constants can be arbitrary.
Observed in the 2016 alignment: TIB glued module 369141860 has its rphi
face rotated ~49 deg against its partner (ideal: exactly anti-parallel),
which contaminates the composite frame used to anchor the glued-surface
reconstruction and turns the module into a propagation booby trap (the
dominant source of the runaway-leg fit failures).

If the aligned composite orientation deviates from the ideal one by more
than 50 mrad -- far beyond any genuine alignment correction -- rebuild
the composite frame anchored on the component face with the smaller
aligned-vs-ideal tilt, composed with the ideal component->composite
transform, and emit a LogWarning. In the 2016 UL alignment this fires
for exactly one module (369141860: mono tilt 0.752, stereo 0.0016);
with the repair, 8 of the 11 hotspot candidate fits succeed even with
legacy forward-only propagation.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EXjsuBb3HHCTK8L1jBx75j
Diagnostic EDAnalyzer used for the dead-module forensics behind the CVH
fit-failure studies. Three modes:

- detids: dump position/normal/glued-partner/isStereo for a given list
  of DetIds, plus SiStripQuality bad-module status.
- scanAllGlued: check parallelism of the two faces of every glued
  composite in the tracker.
- compareIdeal: sweep all modules comparing aligned vs ideal geometry
  (via the "idealForDigi" label), flagging orientation (sin > 0.02) or
  position (> 0.5 cm) outliers.

The 2016 UL census over 16588 sensors found 34 outliers: the one active
booby trap (369141860, TIB, 49 deg), one harmless live tilted module,
and ~30 latent dead FPIX modules outside the acceptance.

BuildFile: add CalibFormats/SiStripObjects + CalibTracker/Records for
the SiStripQuality lookup.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EXjsuBb3HHCTK8L1jBx75j
The pixel hit-quality criterion was hard-coded as
!isOnEdge && sizeX > 1, demoting failing hits to inactive (module
still traversed for material, no measurement). Make both parts
configurable (existsAs-guarded in the base class, defaults preserve
the legacy behaviour bit-identically):

- keepPixelEdgeHits (default False): lift the boundary veto for
  clusters touching the sensor edge.
- pixelMinSizeX (default 2): minimum cluster size in x; 1 admits all
  clusters including single-pixel-in-x.

Both makers print a per-job pixel hit-quality summary
(seen/onEdge/sizeX1/demoted) alongside the fit summary. Exposed in
runCvhJpsi.py and runCvhSingleTrack.py.

Findings on 100k J/psi ALCARECO events (Run2016H):
- 3.7% of valid pixel hits are on-edge, 31.4% fail sizeX>1; about
  half of the edge clusters are also single-pixel in x.
- Edge hits only (keepPixelEdgeHits=True): recovers 1.9% of pixel
  hits; failure rates, at-cap fraction and Jpsi mass unchanged;
  sigma_m -0.3% on affected candidates.
- All pixel hits (pixelMinSizeX=1 + keepPixelEdgeHits=True): hard
  failures down 3x (single-track 36->12 of 197k) with pathological
  fits rescued; core mass resolution -1.4%, sigma(q/p) -1.1%; robust
  mass-scale shift +0.05 +- 0.03 MeV. But limit cycles worsen
  (single-track at-cap 15.8%->17.7%, forward 10.7%->15.5%) and q/p
  on converged gainers moves by median 2 sigma of the baseline error
  with only ~1% error-bar reduction -- the CPE position errors of
  unshared/edge clusters are underestimated. Defaults therefore keep
  both cuts for production.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EXjsuBb3HHCTK8L1jBx75j
Design plan in doc/global-material-model-plan.md: replace ~25k
per-module energy-loss parameters with O(50-100) parameters attached to
physical material volumes (active and passive), evaluated along each
track's actual path -- origin- and direction-independent by
construction. Material analogue of the parmtype-14 scalar-potential
field reduction.

Phase 0 deliverables:
- MaterialAuditAnalyzer: geometry-only DDDWorld + G4Navigator ray
  tally over the tracker (path, x/X0, mass path, Bethe-Bloch dE for a
  10 GeV muon) keyed by (logical volume, z-side, 2 cm r-bin, 10 cm
  z-bin). The eta profile of total x/X0 reproduces the published
  tracker material budget (0.46 X0 at eta 0, peak 2.14 at |eta| 1.5,
  z-symmetric).
- runMaterialAudit.py: EmptySource driver on the same DB XML geometry
  as the CVH refit drivers.
- makeMaterialGroups.py + data/materialGroups{50,100}.txt: the two
  grouping tiers (41 z-symmetric groups / 82 z-split groups + catch-all)
  as ordered classification rules (name regex + r/z windows + z-side).
  Per-step classification is required because logical volumes are
  shared across layers (PixelBarrelActive* serves all three BPIX
  layers; TOBActive name digits are module types spanning layer pairs).

Passive material dominates as expected (TEC structure 14%, TIB supports
13% of total x/X0; each active layer 0.1-1%), which is the point of
the model: the legacy per-module parameters cannot describe it for
displaced or cosmic tracks.

Base.cc: comment note documenting why no separate dead-face repair is
needed in the glued-module guard (plane already rebuilt from the
repaired composite; verified bit-identical on 10k events).

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EXjsuBb3HHCTK8L1jBx75j
…es in the propagator

- MaterialGroupModel (new): runtime classifier mapping a Geant4 step
  (logical volume, midpoint r, z) to a material group via the ordered
  rules of a materialGroups tier file; per-LV regex shortlists are
  memoized, per step only r/z/zside windows are evaluated. Carries the
  per-group scaling values k_g and an FD-injection knob.
- sim::Field: abstract MaterialOffsetProvider next to the existing
  scalar dxi channel (SimG4Core stays independent of TrackPropagation).
- G4ErrorEnergyLossForCVH: when a provider is attached, each step's
  energy loss is scaled by exp(dxi + k_g) with the group resolved from
  the step's own volume -- this makes the material correction
  volume-resolved and hence independent of track origin and direction.
- propagateGenericWithJacobianAltD: two new defaulted arguments
  (matGroups, groupJacOut). Per-group d(state)/dk_g columns are
  accumulated with the same transport recursion as the integrated dxi
  column, so the sum over groups equals jac.col(8) exactly; P-conjugated
  on backward legs; provider set/cleared symmetrically on all exits.
  Null arguments leave the legacy path untouched.

Validation (J/psi ALCARECO): V0 bit-identity with the model off
(19678/19678 tracks on 10k events); V1 FD closure fd/analytic = 1.0005
across 8 streams (forward-difference truncation at eps = 1e-3) with
analytic d(q/p)/dk spanning 5e-8..8e-5; V2 sum identity to 2.6e-15
relative over ~450k propagation legs.

See Analysis/HitAnalyzer/doc/global-material-model-plan.md.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EXjsuBb3HHCTK8L1jBx75j
…n both makers

globalMaterialModel flag (exclusive switch, requires materialGroupsFile):
replaces the per-module material parameters (parmtype 7, one per glued
module) with parmtype-15 sentinel entries, one per material group of the
loaded tier file -- mirroring the parmtype-14 scalar-potential field
modes (sentinel runtree handling, global-index resolution next to
resolveGlobalIndices, corFiles update path).

Gradient assembly (single-track + two-track makers): the per-hit eloss
block becomes nGroups columns -- the propagator's per-group
d(state)/dk_g columns for crossed groups, zeros elsewhere -- with shared
global indices collapsed by the existing index map, the same mechanics
as the field-mode block. doRes resolution-slot offsets generalized.
With the model on, the leg-constant dxi is zero and the per-step
provider applies the current k_g, synced from corparms_ per event.
The MaterialGroupModel is owned by the base class (both makers share
it); the single-track maker keeps the Phase A validation knobs
(materialFDGroup/materialFDEps + inline V2 check).

Validation (J/psi ALCARECO, all k_g = 0): single-track q/p bit-identical
3928/3928 vs legacy registration; dimuon mass+pt bit-identical 1958/1958
vs the legacy 100k baseline; per-track Jacobian grows by exactly the
expected 42-groups-minus-per-module-slots; stored gradient magnitudes
physically ordered (tib_support largest, matching the Phase 0 material
weights); V2 sum identity 2.4e-15 with the model active.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EXjsuBb3HHCTK8L1jBx75j
- materialGroupsFile / globalMaterialModel (+ single-track
  materialFDGroup/materialFDEps validation knobs) for the global
  material model, both drivers.
- useOpera3D now also routes the labelled 160812 field into the CPE
  ESProducers (stripCPEESProducer, StripCPEfromTrackAngleESProducer,
  siPixelTemplateDBObjectESProducer, templates), matching the production
  nano_cff wiring and the 10_6 cross-release drivers -- previously the
  hit re-evaluation used the default field while the propagator used the
  grid. Used for the 10_6-vs-15_0 cross-release validation (agreement at
  few-1e-6 per track on identical input).

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EXjsuBb3HHCTK8L1jBx75j
- Seed the parmtype-15 corparms entries from the k_init column of the
  groups file (genuine multiplicative deltas, unlike the parmtype-14
  absolute coefficients whose seeding stays disabled). corFiles deltas
  accumulate on top; also the injection route for the V3 closure.
- Plan doc: application contract resolved with the measured V3 numbers.
  Re-seed refits once per calibration iteration; the linearized jacref
  application between iterations is good to ~1% of the correction for
  |k| <~ 1% (error growing linearly, ~3% at k = 0.05). Measured caveat:
  the stored Hessians are Gauss-Newton, and with dE ~ e^k the
  own-parameter gradient response is (H + g) k -- verified exactly
  (predicted ratio -0.591 vs measured -0.592 on the raw-data
  tib_support pull), so iteration-1 deltas far from the minimum are
  not final; the corFiles iteration converges regardless.

V3 closure summary (2k J/psi data events, tib_support injections):
single-track fit response k_hat = 0.01013 for injected 0.01; dimuon
unconstrained one-parameter recovery -0.0089; difference-fit identity
dg = H k to 0.1-1% in all off-diagonal components. Fit-side aggregation
+ solve tool: calibration_studies/global_corrections/fit_global_grads.py.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EXjsuBb3HHCTK8L1jBx75j
…-hit propagation

With both global correction blocks (parmtype-14 field modes, parmtype-15
material groups) the propagation-leg boundaries no longer carry any
parameter attribution. Two new options (default off, bit-identical when
unset) complete the decoupling:

perStepFieldModes: the scalar-potential correction is applied per Geant4
step and the per-mode d(state)/dc_i columns are accumulated in the
propagator (sim::FieldModeProvider abstract base next to the material
provider; ScalarPotFieldModeProvider adapter over the basis evaluator,
with corparms passthrough and an FD-injection knob). ONE basis sample
per step, taken at the predicted step midpoint (5 mm lookahead at the
10 mm step cap), feeds BOTH the applied offset and the derivative
columns -- consistent by construction. Removes the per-leg
piecewise-constant sampling error of the legacy chain rule (~0.04 mT at
2 cm legs, the order of the NMR accuracy target). Per-mode columns are
P-conjugated on backward legs like the dB columns.

skipHitlessSurfaces (requires globalMaterialModel): dead-module
placeholders and quality-demoted hits are dropped from the fit hit
list, so propagation goes hit to hit. Statistically exact (process
noise composes across the merged leg); the material of skipped modules
is still crossed by the G4 steps.

Validation (2016 J/psi data): defaults off and perStepFieldModes with
zero coefficients both 100% bit-identical (3928/3928 tracks); per-step
FD closure (provider injection, qop/lambda/phi comparison, per-mode eps
scaled by basis amplitude) closes phi at the forward-FD limit for all
modes and lambda at 1e-6 where non-vanishing; full package leaves
tracks without hitless surfaces bit-identical and shifts the rest by
median 4e-7 relative q/p (<nHits> 18.3 -> 16.5, at-cap unchanged); the
11 dead-module hotspot candidates all succeed with production defaults.

Also: the single-track per-leg FD-closure block compared cartesian
endpoint differences against curvilinear columns (latent bug, never
exercised -- the validated B.5 closure ran in the two-track maker);
the per-step block uses the proper qop/lambda/phi conversion, and
runFDClosure/epsilonFDClosure knobs are now exposed in
runCvhSingleTrack.py as well.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EXjsuBb3HHCTK8L1jBx75j
…tion variances

The same per-step k_g that scales the mean energy loss now coherently
scales the step's multiple-scattering variance, ionization-fluctuation
variance, and radiation-length tally (e^k more material = e^k more
scattering and fluctuation power). Only the CURRENT k values enter --
the two-step scheme decided in the plan: fitted coefficients reweight
the next refit iteration, the fit never differentiates through the
weights. The step-group classification is hoisted ahead of the MS block
and reused by the per-group column accumulation (one classify per step,
new MaterialGroupModel::offsetOf); the legacy dms/dioni knobs compose
on top unchanged.

This restores, group-resolved and origin-independent, the coherence the
legacy per-module scheme had (with doRes off, the module xi scaled
MS/ioni via dms = dioni = dxival) and the M0-only global model had
dropped. Supersedes the per-module resolution parmtypes 10/11 for the
material-driven part; decoupled per-group variance parameters remain a
documented fallback if the M1 resolution closure fails.

Gates (2016 J/psi data, 2k events): k = 0 bit-identical including
covariances (3928/3928); k = +5% injected on TIB supports grows
sigma(q/p) by +0.69% in the barrel, +0.52% in the transition and +0.00%
forward -- the exact eta footprint and magnitude of the TIB-support MS
share; refit central values shift by median 0.12 sigma from the
reweighting.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EXjsuBb3HHCTK8L1jBx75j
- perStepFieldModes now defaults to True (maker + drivers): per-step
  application/attribution is strictly more accurate than the per-leg
  chain rule and bit-identical for the fitted states at zero
  coefficients; the stored parmtype-14 Jacobians change at the per-leg
  sampling-error level.
- skipHitlessSurfaces now defaults to the value of globalMaterialModel:
  hit-to-hit propagation is on whenever the global material model is on
  (the only configuration where it is well-defined), off otherwise;
  explicitly forcing it without the model still throws, and the drivers
  auto-disable their default-True when the model is off.

Direction-mode study under the full package (10k J/psi data,
globalMaterialModel + hit-to-hit + per-step field): single-track fits
are IDENTICAL between alongMomentum and anyDirection (same 4 failures
of 19682, zero backward legs in 2M propagations -- direction choice is
now irrelevant there); the dimuon fit still needs anyDirection for
4/9801 candidates (0.04%, valid-plane order flips after the
vertex-constrained update; 8 backward legs in 1M propagations recover
all of them). Forward-only dimuon failures drop 0.26% -> 0.04% relative
to the pre-package code: the dead-module phantom-plane class is
eliminated at the source, anyDirection remains as a free safety net.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EXjsuBb3HHCTK8L1jBx75j
globalMaterialModel now defaults to True in both CVH drivers, with
materialGroupsFile defaulting to the tier-50 grouping file resolved
from the release (src/Analysis/HitAnalyzer/data/materialGroups50.txt).
Together with the existing defaults this brings up the complete
leg-structure-free configuration with no extra arguments: parmtype-15
material groups (replacing the per-module block), per-step field
attribution, hit-to-hit propagation, and M1 coherent variance scaling.
Set globalMaterialModel=False for the legacy per-module
parameterisation; materialGroups100.txt selects the relaxed tier.

Deliberately driver-level only: the maker-side default stays off, so
non-driver configurations (e.g. the NanoAOD production customisation)
keep the legacy parameter model until the coordinated migration.

Smoke (500 events, no material arguments): 42 parmtype-15 groups
registered, zero parmtype-7 entries remaining, 124/124 candidates fit.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EXjsuBb3HHCTK8L1jBx75j
…nels

Candidate-driven driver over the stage-1 V0 ALCARECO (persisted
*Resonances collections), KS -> pi pi and Lambda -> p pi, using the
existing PiPi / ProtonPi cfis. Full global-model defaults (parmtype-15
material groups, per-step field modes, hit-to-hit propagation) -- the
first consumer of the displaced-track channels the origin-independent
corrections were built for. Geant4ePropagator momentum floor exposed as
ptotLimit, default 0.05 GeV (the 10_6-era V0 lesson: soft daughters
dominate the failure accounting at the 0.5 GeV default).

First results (2016G Charmonium stage-1 test sample, repacked split=1):
- KS: 11 037 candidates, 3.65% failures (perigee starting state);
  core mass 497.57 MeV (PDG 497.611), 79% core fraction.
- Lambda: 4 230 candidates, 13.2% failures; core mass 1119.2 MeV
  (PDG 1115.7), 73% core fraction.
- Failure mix (KS): prop 2.0% (fieldbound runaway legs dominant),
  NaN updates 1.0%, charge flips 0.65% -- candidates for porting the
  single-track momentum-floor clamp to the two-track maker.
- useStartingState default 'perigee': measured BETTER than
  'midPropagated' on V0s (3.65% vs 7.20%) despite the latter being
  built for displaced starting states -- to be understood.

CRITICAL for any 10_6-produced V0 ALCARECO (including the current
stage-1 workflow, conditions 106X): the SiStripCluster branches are
written at splitLevel=99 and deserialize EMPTY in 15_X (ROOT cms-sw#19773) --
the refit then fails at 98.7% with iteration-0 divergence. Repack in
10_6 with PoolOutputModule splitLevel=0 +
overrideInputFileSplitLevels=True before running the 15_0 CVH, or
produce the stage-1 with 15_X.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EXjsuBb3HHCTK8L1jBx75j
Port of the single-track safeguard: when an update would flip a
daughter's q/p sign (diverging step) or push its momentum below the
configurable floor, the WHOLE joint step vector (both tracks + vertex,
one coupled system) is rescaled by a common factor -- direction
preserved, halfway to q/p = 0 on a flip, exactly onto the floor
otherwise. Applied before the deltachi2/EDM bookkeeping; the charge-sum
abort remains as backstop. clampMomentumFloor default 2 GeV (the
single-track value, J/psi semantics unchanged); the V0 driver passes
0.1 GeV (above the propagation floor, below the soft-daughter
spectrum). clamped[step] counter added to the fit summary.

Validated on the 2016G SingleMuon V0 ALCARECO (pmlugato production):
charge-flip aborts eliminated completely (KS 225 -> 0 of 24855
candidates, Lambda 81 -> 0 of 10444); total failures KS 4.57% -> 4.11%,
Lambda 9.90% -> 9.44%. About half the clamped fits converge to
candidates of usable quality (~50% in the signal mass core, median on
the peak); the rest continue and fail later in propagation, now
correctly classified under fail[prop] instead of aborting at the
update.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EXjsuBb3HHCTK8L1jBx75j
…e NaN class)

Root cause of the fail[nan] class, localized by instrumenting the solve
abort on the V0 samples: in all 161 KS cases the residuals and
Jacobians were finite and only the weight matrix was not, for
candidates with 1-3-valid-hit daughters at decay radii ~5 cm -- a
displaced V0 vertex sitting on the first-hit layer gives a
(near-)zero-length first leg, Q ~ 0, and the plain Q.inverse() poisons
the normal matrix with inf at the very first iteration (101/161 at
icons=0, iiter=0).

Fix: invert the per-leg process noise through its eigendecomposition
with the eigenvalues floored at 1e-10 of the largest (absolute floor
1e-16). The zero-noise leg becomes an extremely stiff constraint
instead of an exact one -- the mathematically correct limit (two states
joined by a zero-length leg are rigidly coupled) with a finite weight.
Normal legs sit far above the floor and are untouched at double
precision.

Validation (pmlugato 2016G V0 files): NaN failures 161 -> 0 (KS,
24855 candidates) and 163 -> 0 (Lambda, 10444); the prop class also
improves (sane weights help marginal fits); total failures
KS 4.11% -> 3.33%, Lambda 9.44% -> 7.67%. Recovered candidates are
genuine on-layer decays: 53% (KS) / 65% (Lambda) in the signal mass
core, medians on the peak. Combined with the momentum-floor clamp the
V0 failure budget is now purely propagation physics
(KS 4.57 -> 3.33%, Lambda 9.90 -> 7.67% overall; chargeflip and nan
both exactly zero).

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EXjsuBb3HHCTK8L1jBx75j
…tion)

The remaining V0 propagation failures split into two mechanisms with two
remedies, both budgeted per candidate and honestly accounted:

- Backtracking (failures at iiter > 0; dominant for KS): a runaway or
  drained leg after an update means the too-large Gauss-Newton step left
  the propagatable manifold -- the previous iterate propagated fine by
  construction. Restore the iteration-entry linearization state
  (reference states + per-hit layer states, snapshotted before the
  reference update) and redo the iteration with the step halved (up to
  4 halvings). Trust-region behaviour: the fit only takes steps whose
  trajectories propagate.
- Seed inflation (failures at iiter == 0; dominant for Lambda): the
  drained daughter is overwhelmingly the PROTON (median pT 0.29 GeV,
  beta ~ 0.35, ~60 MeV kinetic energy against 1/beta^2-boosted dE/dx)
  -- an end-of-range particle whose modeled energy loss consumes the
  seed trajectory anywhere along the path (flat ihit distribution).
  Inflate the failing daughter's seed momentum by 1.25x (up to twice)
  and let the hits pull it back down.

Validation (pmlugato 2016G V0 files): KS failures 3.33% -> 2.48%
(2814 halvings, 92 inflations), Lambda 7.67% -> 4.11% (629 inflations
recovering the slow-proton class, 2016 halvings). Recovered candidates
are genuine: 51%/62% in the signal mass core, medians on the peak.
Cumulative V0 campaign: KS 4.57% -> 2.48%, Lambda 9.90% -> 4.11%, with
chargeflip and nan classes at exactly zero and every remaining failure
a budget-exhausted propagation case. backtracked[step] and
inflated[seed] counters added to the fit summary.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EXjsuBb3HHCTK8L1jBx75j
…wiring

Replace the per-producer edm::GlobalCache<CvhMasterThread> with a single
shared Geant4 master exposed as an EventSetup product, and wire the CVH
muon refit into the custom NanoAOD (Category A of the 10_6 -> 15_0 port).

Shared master (the enabler):
- New CvhMasterRecord (DependentRecord over IdealGeometry + IdealMagneticField)
  + CvhMasterESProducer producing shared_ptr<CvhMasterThread> once per job,
  consumed by every maker via esConsumes. TYPELOOKUP_DATA_REG(CvhMasterThread).
- CvhMasterThread: drop self-esConsumes/callConsumes; ensureG4Started() now
  takes the fetched geometry/field products (pointer-compare rebuild guard).
- ResidualGlobalCorrectionMakerBase is now edm::stream::EDProducer<> (dropped
  GlobalCache + RunCache + the four global* methods + callConsumes); both
  makers' produce() use iSetup.getData(cvhMasterToken_).cvhMaster().
This lets heterogeneous makers coexist in one job (single-track nominal/ideal/
bs, two-track, V0, B->J/psi K) -- a second GlobalCache master would have hit
the G4MTRunManagerKernel singleton and aborted.

NanoAOD wiring (muons_cff.py / nano_cff.py):
- tracksfrommuons + trackrefit (nominal, data+MC); MC-only trackrefitideal,
  trackrefitbs and mergedGlobalIdxs. Muon table gains cvh*/cvhideal*/cvhbs*
  scalar + vector branches (nominal+ideal reproduce the 10_6-tip contract; bs
  is a self-contained parallel set).
- nano_cff.setup3DFieldForRefit brings up the ScalarPot3D field + Geant4e
  propagator + the shared cvhMasterESProducer and routes the field label /
  init file / per-stream RNG to all refit instances; entry points
  nanoAOD_addCvhMuon (data) and nanoAOD_addCvhMuonMC (MC).
- Drivers runCvhJpsi/V0/SingleTrack/BplusJpsiK instantiate cvhMasterESProducer
  instead of the per-producer CvhMaster PSet. New runCvhCoexist.py test.

Validated: scram b green (el9); runCvhJpsi regression identical (J/psi fits
100% success); runCvhCoexist runs two-track + nominal/ideal/bs single-track
in ONE job on one master (0 aborts, all fit summaries 100%); edmConfigDump of
data + MC NANO configs expands with all producers/branches and one master.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TwFg9M7j9LonzrmRemvsrL
Expose the per-candidate backtrack / seed-inflation budgets of the
leg-failure recovery block as maxBacktracks (default 4) and
maxSeedInflations (default 2) parameters instead of the hardcoded
literals, and surface them as VarParsing options in runCvhV0.py so V0
channels can tune the retry depth.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TwFg9M7j9LonzrmRemvsrL
The MC CVH refit must use the same default CMSSW field as the simulation
that generated the sample, not the accurate ScalarPot3D map (which is a
data-only correction) -- see CLAUDE.md "custom field map for data only".

setup3DFieldForRefit gains useScalarPot3D (default True): when False (MC)
it no longer installs ScalarPot3DMagneticFieldProducer and does not route a
field label onto the propagator / geopro / CPEs / makers / shared G4 master,
so they all keep the default IdealMagneticFieldRecord field. The parmtype-14
correction-mode basis (scalarPotentialInitFile on the makers) is still set in
both cases -- it defines the Jacobian modes stored in NanoAOD and is
independent of the baseline field. nanoAOD_addCvhMuonBranches defaults
useScalarPot3D to (not isMC), so nanoAOD_addCvhMuon (data) uses ScalarPot3D
and nanoAOD_addCvhMuonMC (MC) uses the default field; overridable.

Verified via edmConfigDump: data installs ScalarPot3D and labels every CVH
consumer ScalarPot3DMf; MC installs no ScalarPot3D producer, leaves the
propagator/master field label empty (default), and still wires the basis +
nominal/ideal/bs refits.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TwFg9M7j9LonzrmRemvsrL
Add the two-track (dimuon) CVH refit to the MINIAOD custom NanoAOD, as a
generic resonance-agnostic "Dimuon" candidate table (works for Z / J/psi /
Upsilon -- the resonance is selected only by the candidate producer's mass
window; the refit itself is a plain common-vertex fit, no mass constraint).

- New DiMuonTrackVertexCandidateProducer: opposite-sign muon-track pairs
  (from tracksfrommuons) in a configurable mass window -> a
  reco::VertexCompositeCandidateCollection with RecoChargedCandidate daughters,
  the form the two-track refit consumes on its srcCandidates path.
- ResidualGlobalCorrectionMakerTwoTrackG4e: new produceValueMaps flag (default
  False, so the ALCARECO TTree drivers are unchanged) makes it emit per-candidate
  EDM ValueMaps keyed to srcCandidates -- kinematics always (corMass/corMassErr/
  corPt/eta/phi + per-muon refit pt/eta/phi + edmval), and the global-fit payload
  (globalIdxs/jacRefMu{Plus,Minus}/jacMass/factored Hessian) additionally when
  fillGradsFactored is on. Sentinel-filled, one entry per input candidate.
- Generic ResidualGlobalCorrectionMakerDiMuonG4e cfi + a Dimuon
  SimpleCandidateFlatTableProducer (CandVars + cvh* externalVariables) and a
  FlattenedCandValueMapVectorTableProducer for the payload; wired into
  nanoAOD_addCvhMuonBranches (data + MC) and setup3DFieldForRefit (field label /
  init file / RNG). Shares the one Geant4 master with the single-track refit
  (Stage A2). Data -> ScalarPot3D, MC -> default field.

Validated: build green; data + MC edmConfigDump wire the Dimuon table + candidate
producer + refit (field correct per data/MC; payload table scheduled only when
the flag is on); runtime on J/psi ALCARECO (runCvhDimuon.py) yields cvhMass
peaking at the J/psi (mean 3.07 GeV) with filled per-muon momenta and a
consistent 3 x nGlobalIdx Jacobian payload, 0 fit failures.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TwFg9M7j9LonzrmRemvsrL
The CVH fit state is (q/p, lambda, phi, d0, z0) at the PCA to the beamspot, but
the maker only ever filled the momentum part -- refParms[3]/[4] (d0, z0) were
left at 0 behind a longstanding "fix filling of position parameters" TODO, so
the refit impact parameters were discarded.

Fill refParms[3]/[4] from the converged state (cart2pca(refFts)) plus the
reference-block update dxref[3]/[4], mirroring exactly how the momentum
parameters are filled. Emit them as new corDxy / corDz ValueMaps and add
Muon_cvhDxy / Muon_cvhDz (and cvhideal*/cvhbs* via the shared scalar-var
helper) to the muon table.

Impact parameters are w.r.t. the beamspot PCA (the fit reference); meaningful
on the unconstrained trackrefit (the trackrefitbs variant constrains them).

Validated on the hit-retained DY MINIAODSIM: cvhDxy tracks the muon's own
dB(BS2D) to the micron level (median |cvhDxy - dB(BS2D)| = 2.7 um over 213
refit muons), cvhDz physical.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TwFg9M7j9LonzrmRemvsrL
…handling fixes

Single-track maker (ResidualGlobalCorrectionMakerG4e + Base):

- One-pass two-hypothesis charge resolution: when the nominal fit's clamp
  catches a q/p sign crossing (nChargeFlipProtect>0 -- the fit "wanted" the
  opposite charge, the high-p mis-ID signature), re-fit the opposite charge
  and keep the lower-chi2 result. The per-track fit body is wrapped in a
  runHypothesis lambda so it can be run per charge in a single pass; only
  refFts is snapshot/restored. Gated by twoHypothesisCharge (default on);
  inert for well-measured (low/mod p) tracks that never trip the clamp.
  New branches nChargeFlipProtect, chargeHypFlipped; counter chargehyp[flipped].

- allowChargeFlipAboveP knob (default 1e9 = legacy/off): separates the always-on
  p>=2 GeV momentum floor from the sign-flip clamp, permitting genuine charge
  flips above a momentum. Kept as a study facility -- naive step-crossing
  diverges, so the two-hypothesis refit is the production path.

- forceCosmic knob (default off): apply the outside-in cosmic handling
  regardless of track algo. Needed for split-track legs (cosmicTrackSplitter
  output, algo 'cosmic') which were otherwise fit with the beamline-PCA
  parameterisation; improves split-leg fit success and removes spurious
  backward legs from the mid-trajectory PCA reference.

- cosmicSeedFromEntry knob (default off): seed cosmic legs from the muon-entry
  (higher-y) end and iterate hits downward -> fully forward propagation.

Drivers (Analysis/HitAnalyzer/test):

- runCvhCosmics.py: single-track CVH refit of cosmic ALCARECO
  (TkAlCosmicsInCollisions), field-status good-run filter, full global model.
- runCvhCosmicsSplit.py: split upper/lower-leg test -- refit -> CosmicTrackSplitter
  -> refit each leg -> two independent single-track CVH fits for resolution /
  coherent-bias checks.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LTMiAXeSNPnpgFd5Juk8jE
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.

6 participants