Skip to content

Mu2eKinKal clone copy constructors drop state; KKMaterial eloss-mode cast and a dangling HeCF4 material #2007

Open

Description

@oksuzian

These five defects came out of a static sweep of KFTrack/KinKal (KFTrack/KinKal#225). They are
in Offline's code and data, not in KinKal, so they're filed here. I rechecked all five at Offline
main 7bcb06ff67a4b97b2fd20418e89c1449136229e7.

None of them affects a production job today. Items 1 and 2 matter once anything calls
KinKal::CloneContext, which is the track re-instantiation path that KFTrack/KinKal#224 is staging.
Nothing in Offline calls it yet. Every mention of CloneContext is inside Mu2eKinKal/inc/KKTrack.hh (the copy constructor at :77 and two helpers at :103 and :116), and no .cc file constructs one.


1. 🟡 [S2] KKStrawHit and KKCaloHit copy constructors skip their ResidualHit base, so clone() drops the hit weight

Mu2eKinKal/inc/KKStrawHit.hh:55-67 and Mu2eKinKal/inc/KKCaloHit.hh:31-37 both list their own
members and never name KinKal::ResidualHit<KTRAJ>(rhs). The base is therefore
default-constructed through its protected ResidualHit() {}, and the cached weight_ comes out
empty. clone() in both classes is std::make_shared<T>(*this), so every clone goes through this
constructor.

KinKal makes delegating to the base copy constructor opt-in (Detector/ResidualHit.hh:14). I
reproduced the effect with a minimal subclass that skips it. The clone reports the same
active(), the same nDOF() and the same residual as the original, but with zero weight:

good clone   active=1 nDOF=1 resid.value=0.7 |weightVec|=82.0359 trace(weightMat)=3.64
bad  clone   active=1 nDOF=1 resid.value=0.7 |weightVec|=0       trace(weightMat)=0

Nothing downstream can tell the bad clone from a good one. That demo used a stand-in class, not
the two headers above. The headers were read, not run.

Suggested fix: add KinKal::ResidualHit<KTRAJ>(rhs), at the head of both initializer lists.
KKStrawHit.hh:47 already declares using RESIDHIT = KinKal::ResidualHit<KTRAJ>; and never uses
it, so that alias is available.


2. 🟡 [S2] KKStrawXing copy constructor loses the updater configuration, the material xings and the active flag

Mu2eKinKal/inc/KKStrawXing.hh:39-50 delegates to the four-argument constructor. That
constructor's fifth parameter, active, defaults to false (:34), and none of sxconfig_,
mxings_, fparams_ or active_ is copied. clone() at :51-67 patches only shptr_ and the
trajectory in ca_.

I ran this by compiling the real KKStrawXing.hh and KKStrawMaterial.cc against KinKal
134126b5, with a Mu2e straw and a 100 MeV/c electron:

src               active()=true   config().nsig_=3   config().maxdoca_=5  matXings().size()=2  |params()|=3.540e-02
XING copy(src)    active()=false  config().nsig_=-1  config().maxdoca_=0  matXings().size()=0  |params()|=0.000e+00
src.clone(ctx)    active()=false  config().nsig_=-1  config().maxdoca_=0  matXings().size()=0  |params()|=0.000e+00

nsig_=-1 and maxdoca_=0 are the StrawXingUpdater defaults (StrawXingUpdater.hh:10-11). The
nsig_ < 0 guard at KKStrawXing.hh:158 sits inside if(first). The normal meta-iteration
update passes first == false, so the clone doesn't throw. It carries on with an empty
configuration.

This breaks KinKal's own contract for the method it overrides. Detector/ElementXing.hh:48-54
says an overriding clone() must reassign the pointer members explicitly.

Suggested fix: copy the four members in the copy constructor, and pass rhs.active() through.


3. 🟡 [S2] KKMaterial.cc:20 casts an unbounded fhicl int straight into a three-valued enum

dmconf.elossmode_ = (DetMaterial::energylossmode)matconfig.elossMode();

elossMode is fhicl::Atom<int> (KinKalGeom/inc/KKMaterial.hh:39), with no bound in its type
or its Comment. DetMaterial::energylossmode is {mpv=0, moyalmean, bethemean}. KinKal's
consumer (MatEnv/DetMaterial.cc:134-140) is an if/else-if chain with no invalid case, so any
other value falls through to MPV. Running it through KinKal:

setElossMode(2)  -> ionizationEnergyLoss = -43.06791841
setElossMode(3)  -> ionizationEnergyLoss = -38.06825137   <== out of range, silently == mpv
setElossMode(7)  -> ionizationEnergyLoss = -38.06825137   <== out of range, silently == mpv

KinKalGeom/fcl/prolog.fcl:15 sets 1, so nothing is wrong today. But
IonizationEnergyLossMode : 3 in any job would quietly change the energy-loss model with no
diagnostic.

Suggested fix: reject values outside {0,1,2} in the KKMaterial constructor, or better, make
it a named enum parameter so fhicl validation rejects it.


4. 🟡 [S2] TrackerConditions/data/MaterialsList.data:19 defines HeCF4 from a CF4 that exists nowhere

HeCF4  0.0005331 0. 0. 2 0.334 Helium 0 0.666 CF4 1 -10 -20 -30 20.0 1.0 gas

CF4 is not defined in this file or in ElementsList.data. The file that
KinKalGeom/fcl/prolog.fcl:4-6 points MatEnv at is this one. KinKal's own copy of the list still
defines CF4 (in its ITracker block), and Offline's copy dropped it. KinKal resolves an unknown
component to a null pointer without throwing. Asking for HeCF4 against the production data
segfaults inside MtrPropObj. The KinKal side of that is item 1 on KFTrack/KinKal#225.

Nothing asks for it: git grep HeCF4 returns only this line, and every material name in the
prolog and in KinKalGeomMaker.cc resolves. Remove the row, or restore a CF4 definition.


5. ⚪ [S3] Analyses/test/materailsPropStudy.fcl:47 names a module that does not exist

module_type : materailsPropStudy, and no source file by that name exists anywhere in the tree.
The job cannot run. The file supplies a free-form material name from fhicl (:49), which makes
it the one place item 4 could have been reached, but it is dead.


Not checked: no art job was run. Items 1 and 2 matter only once CloneContext has a caller.
The executed output for item 1 comes from a stand-in class, and for item 3 from running KinKal
directly rather than through KKMaterial.

Activity

  1. oksuzian commented on Sep 24, 2026

    @oksuzian
    CollaboratorAuthor

    One more item for this issue, in the same Mu2eKinKal code. I found it while measuring KFTrack/KinKal#225 item 2 (see KFTrack/KinKal#225, comment 5817222595). I established it by reading the code at Offline main acc62589e19d122ba5880bc49a9f425f1e4f1887; I did not run it.

    6. 🟡 [S2] KKStrawHit::updateReference checks the branch of the old closest approach, not the new one

    Mu2eKinKal/inc/KKStrawHit.hh:164-172:

    PCA pca(ptraj,wirePtr(),tphint,precision());
    // check that we're on the right branch: we can move off if t0 changes a lot between iterations
    double dz = straw().origin().z() - ca_.particlePoca().Z();      // :166, old ca_
    double maxdz(100.0);
    if((!pca.usable()) || fabs(dz) >  maxdz) {
      ...
      pca = PCA(ptraj,wirePtr(),tphint,precision());
      dz = straw().origin().z() - pca.particlePoca().Z();            // :171, new pca
      if((!pca.usable()) || fabs(dz) >  maxdz) whstate_.state_ = WireHitState::unusable;
    }
    ca_ = static_cast<CA>(pca);                                       // :174

    The comment says the test exists to catch the new solution moving onto the wrong helix loop. But :166 measures dz from ca_, the previous iteration's closest approach, which :174 only replaces afterwards. The retry path at :171 does use pca, which shows the intent. As a result:

    • A new pca that has jumped to a neighbouring loop is accepted in this iteration. It is caught only in the next call, if there is one.
    • On the final call of a fit nothing checks it, so a hit can be persisted with a closest approach on the wrong loop, displaced by roughly one helix pitch along z.
    • In the other direction, a hit whose previous solution was off-branch is re-seeded even when the new pca is already correct. That costs time but gives no wrong answer.

    Suggested fix: double dz = straw().origin().z() - pca.particlePoca().Z(); at :166.

    Not checked: how often a closest approach actually jumps loops between iterations in production fits. That decides whether this ever fires. This finding comes from reading the code, not from running it.

  2. edcallaghan commented on Sep 25, 2026

    @edcallaghan
    Contributor

    @brownd1978 do you have an opinion on item-2 here? IIRC we decided a while back that the missing state-copies were ok because they would be reassigned by any non-zero schedule ran after the clone.

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions