Calo mc rev - #1919
Conversation
|
Hi @bechenard,
which require these tests: build. @Mu2e/write, @Mu2e/fnalbuild-users have access to CI actions on main. ⌛ The following tests have been triggered for ea62775: build (Build queue - API unavailable) |
|
☔ The build tests failed for ea62775.
N.B. These results were obtained from a build of this Pull Request at ea62775 after being merged into the base branch at 1ce31db. For more information, please check the job page here. |
|
@FNALbuild run build test |
|
⌛ The following tests have been triggered for 1bce0f9: build (Build queue - API unavailable) |
|
☀️ The build tests passed at 1bce0f9.
N.B. These results were obtained from a build of this Pull Request at 1bce0f9 after being merged into the base branch at 1ce31db. For more information, please check the job page here. |
oksuzian
left a comment
There was a problem hiding this comment.
PR Review Summary
Mu2e/Offline #1919 — "Calo mc rev" (bechenard, branch CaloMCRev, base main)
Reviewed at head 1bce0f9b
Decision
- 🔴 request changes
Scope understood
- Refactor of
CaloMC(all five producers), extraction of the calo pulse/noise caches intoMu2eUtilities(CaloPulseShape→CaloPulseUtil, newCaloNoiseUtil), and deletion ofCaloNoiseSimGenerator/CaloWFExtractor. - The noise model changes source: instead of being generated per job from
elec/rin/darkrates ×MeVToADC/readoutPEPerMeV, it is now read by default (generate: false) from a new checked-in histogramCaloReco/data/ReadoutNoise.root. The "salt and pepper" random-noise path (addRandomNoise) is removed. - Large amounts of genuinely dead code are removed (diagnostic
TH1F/TH2Fblocks,PhysicalVolumeMultiHelper/caloMaterialmachinery inCaloShowerStepMaker,plotWF). Author states the RNG sequence changes so validation will differ, but "physics remains the same", and explicitly asks for the CMake files to be checked.
Findings
-
🔴 [S0]
CaloTemplateWFProcessordropsminPeakAmplitude— every argument toCaloTemplateWFUtilshifts by one- Evidence:
CaloReco/inc/CaloTemplateWFUtil.hh@1bce0f9bCaloTemplateWFUtil(const CaloPulseUtil::Config& configPulseCache, double minPeakAmplitude, double minDTPeaks, int printLevel=-1);
CaloReco/src/CaloTemplateWFProcessor.cc@1bce0f9bOnly three arguments are passed. The pre-PR call site passed six and includedfmutil_ (config.pulseCache(),minDTPeaks_,config.fitPrintLevel()),minPeakAmplitude_:Resulting binding insidefmutil_ (config.pulseFileName(),config.pulseHistName(),minPeakAmplitude_, config.digiSampling(),minDTPeaks_,config.fitPrintLevel()),CaloTemplateWFUtil, withCaloReco/fcl/prolog.fclTemplateProcessorvalues (minPeakAmplitude: 24,minDTPeaks: 20,fitPrintLevel: -1):member intended actual at head minPeakAmplitude_24 20 (gets minDTPeaks_)minDTPeaks_20 -1 (gets fitPrintLevel)printLevel_-1 -1 (defaulted — masks the shift) Both are live in CaloReco/src/CaloTemplateWFUtil.cc::selectComponent:if (tempPar[ip] < minPeakAmplitude_) return false; // line 229 if (dt < minDTPeaks_ && tempPar[ip2] > tempPar[ip]) return false; // line 238
- Impact: silent calorimeter waveform-reconstruction regression on every job using
processorStrategy: "TemplateFit"(the production default). The fitted-component amplitude cut drops 24 → 20 ADC, and the minimum peak-separation veto becomesdt < -1, i.e. never true — the pile-up peak-merging protection is entirely disabled, so spuriously close fitted components are now all kept. All arguments aredouble/int, so this compiles clean and CI (return-code-only tests) cannot see it.printLevel_coincidentally landing on the same value as the prolog'sfitPrintLevelremoves the one symptom that would have been noticed. - Suggested fix: pass all four arguments —
fmutil_ (config.pulseCache(), minPeakAmplitude_, minDTPeaks_, config.fitPrintLevel()),
Consider dropping theprintLevel=-1default argument so an under-supplied call cannot compile again.
- Evidence:
-
🔴 [S0]
install(DIRECTORY data ...)added to the wrong package:CaloMChas nodata/, and the newCaloReco/data/ReadoutNoise.rootis never installed- Evidence:
CaloMC/CMakeLists.txt@1bce0f9b(added in1bce0f9b, "Added missing dir"):install(DIRECTORY data DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/Offline/CaloReco)
CaloMC/contains onlyCMakeLists.txt fcl inc src test— there is noCaloMC/data.CaloReco/CMakeLists.txtis not touched by this PR and has noinstall(DIRECTORY data ...)line at all.- Reproduced the CMake behaviour locally (cmake 3.31.8):
install(DIRECTORY data ...)on a non-existent directory fails at install time —
CMake Error at build/cmake_install.cmake:41 (file): file INSTALL cannot find ".../data": No such file or directory,cmake --installexit code 1. - Repo idiom check: of the 17 Offline packages that own a
data/directory (CaloConditions, CaloFilters, CalorimeterGeom, CalPatRec, CRVConditions, CRVResponse, DbService, EventGenerator, GlobalConstantsService, Mu2eG4, Mu2eKinKal, Mu2eUtilities, ParticleID, TrackerConditions, TrkHitReco, TrkPatRec, CaloReco), every one exceptCaloRecocarries its owninstall(DIRECTORY data DESTINATION .../Offline/<Pkg>).
- Reproduced the CMake behaviour locally (cmake 3.31.8):
- Impact: two separate breakages in the CMake half of the dual build. (a)
cmake --installof Offline now errors out. (b) Even once (a) is worked around,share/Offline/CaloReco/data/ReadoutNoise.rootis absent from the install tree, soCaloNoiseUtil::fillCache()'sConfigFileLookupPolicycannot resolve the fcl path"Offline/CaloReco/data/ReadoutNoise.root"and throwscet::exception("NOISEREADER")— i.e. everyCaloDigiMakerjob with the defaultaddNoise: truedies at first event in a CMake-installed release. CI cannot catch either:mu2e/buildtestbuilds with scons (scons.log), whereConfigFileLookupPolicyresolves the file straight out of the source tree viaMU2E_SEARCH_PATH, which is exactly whyceDigi/ceMix/ceSimRecoare green at this head. - Suggested fix: delete the line from
CaloMC/CMakeLists.txtand add toCaloReco/CMakeLists.txt:Do not also addinstall(DIRECTORY data DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/Offline/CaloReco)
configure_file(... ${CURRENT_BINARY_DIR} ...)staging lines — that legacy idiom was explicitly dropped alongsideinstall(DIRECTORY data ...)in Offline #1914.
- Evidence:
-
🟠 [S1]
CaloNoiseUtilbatch selection divides bybasetwice, andprepare()makes the per-readoutpeToADCa no-op after the first call- Evidence:
Mu2eUtilities/src/CaloNoiseUtil.cc@1bce0f9bvoid CaloNoiseUtil::prepare(int histoID, double peToADC) { int baseID = histoID / base; // already divided if (baseID == histoBaseID_) return; // early-out if (generate_) generateCache(baseID, peToADC); else fillCache(baseID); } ... void CaloNoiseUtil::fillCache(int histoBaseID) { ... if (hid/base != histoBaseID/base) continue; // divides the already-divided value again
noiseSegment()has the same double-division (int baseID = histoID/base; ... fillCache(baseID)), andhistoBaseID_is stored as the already-divided value. - Impact:
- Wrong batch, silently.
fillCache(baseID)selects histograms withhid/base == baseID/base. That is only correct forbaseID == 0. For anybaseID >= 1(i.e.histoID >= 10000) it evaluates tohid/10000 == 0, so the cache is filled with batch 0 and the requested batch is silently never loaded —noiseSegmentthen throwsCALONOISEUTIL histoID ... is invalid, or worse, returns batch-0 noise. Latent today only becauseCaloDigiMakerhardcodesconst int NoiseWFID(0); it becomes live the moment per-readout noise IDs arrive, which is the stated purpose of the class. peToADCis dead after the first call.CaloDigiMaker::generateSpotNoise()recomputesreadoutScaleFactor(iRO, conds)per readout and passes it toprepare(0, scaleFactor), butprepare()early-returns for every readout after the first, so withgenerate: truethe whole detector is scaled by the first SiPM'sADCPerMeV/pePerMeV; with the prolog defaultgenerate: falsethe argument is ignored outright. The per-readoutreadoutScaleFactorcall in the noise path is therefore pure cost with no effect.
- Wrong batch, silently.
- Suggested fix: keep one convention — either pass the raw
histoIDintofillCache/generateCacheand divide once inside, or pass the already-dividedbaseIDand comparehid/base != histoBaseID. Then either drop thescaleFactorargument fromgenerateSpotNoise/prepare(documenting that noise amplitude is absolute ADC), or key the cache on(baseID, peToADC)so a changed scale actually regenerates.
- Evidence:
-
🟠 [S1] No physics evidence offered for a change that replaces the noise model and its pedestal definition
- Evidence: PR body — "the new noise sampler invalidates the RNG sequence so the validation should fail ..., but the physics remains the same" — with no supporting numbers. The substantive changes are: (a) noise waveform sourced from
CaloReco/data/ReadoutNoise.rootinstead of being generated fromelecNphotPerNs/rinNphotPerNs/darkNphotPerNs×MeVToADC/readoutPEPerMeV; (b) pedestal changes from the closed-formtrunc(noiseRinDark*digiSampling*Σpulse*scaleFactor)(oldCaloNoiseSimGenerator::generateWF) totrunc(mean of histogram bin contents)(CaloNoiseUtil::fillCache); (c)readoutPEPerMeVCsI/readoutPEPerMeVLysodeleted fromCaloReco/fcl/common.fcl, so the previous absolute normalisation is gone; (d) theaddRandomNoisesalt-and-pepper path is deleted outright. - Impact: the pedestal is written into every
CaloDigiand subtracted before peak extraction, and the noise RMS drives theminPeakADC/minPeakAmplitudeefficiency turn-on. Because the RNG re-sequencing makes the standard validation diff uninformative, there is currently no way for a reviewer to distinguish "same physics, different seeds" from "shifted pedestal / different noise RMS". Combined with finding 1, the reco side is changing at the same time, so a single post-fix comparison is needed. - Suggested fix: after fixing findings 1–3, post the before/after distributions that the RNG change does not invalidate: pedestal value, digitized-waveform RMS in empty samples,
CaloHitmultiplicity and energy spectrum, and reconstructed-peak multiplicity per crystal. Also state howReadoutNoise.rootwas produced (which generator settings, doc-db reference) so it can be regenerated.
- Evidence: PR body — "the new noise sampler invalidates the RNG sequence so the validation should fail ..., but the physics remains the same" — with no supporting numbers. The substantive changes are: (a) noise waveform sourced from
-
🟡 [S2]
CaloHitTruthMatchswaps a tolerantoperator[]for.at()on the shower map- Evidence:
CaloMC/src/CaloHitTruthMatch_module.cc@1bce0f9bpreviouslyconst auto& sortedSims = caloShowerSimsMap.at(hit.crystalID());
caloShowerSimsMap[hit.crystalID()]on astd::map, which default-constructed an empty vector and let the hit fall through to the existing"hit not matched"diagnostic path. - Impact: a
CaloHitin a crystal with no survivingCaloShowerSimnow raises an uncaughtstd::out_of_range(art aborts the job) instead of producing an emptyCaloHitMC. Today the invariantcrystals(CaloHit) ⊆ crystals(CaloShowerSim)happens to hold becauseCaloShowerROMakeremitsCaloShowerROandCaloShowerSimfrom the same step loop, andCompressDigiMCs's calo path copies everyCaloShowerSim. But nothing enforces it:caloShowerSimCollectionis externally repointable and is repointed in production —Production/JobConfig/recoMC/epilog.fclsets it to"compressDigiMCs"whilecaloHitCollectionstaysCaloHitMaker, so the two collections are no longer produced by the same module in the same job. Any future pruning (e.g. thecaloClusterMCTagcompression path) turns an unmatched hit into a crash.caloHitMap.at()on the next line is safe — that map is built fromcaloHitsitself. - Suggested fix: use
find()and fall through to the unmatched path, or keep.at()but wrap it in an explicitcet::exceptionwith a message naming the two input tags, so the failure is diagnosable rather than a barestd::out_of_range.
- Evidence:
-
🟡 [S2] Configuration knobs silently demoted to hardcoded constants
- Evidence:
CaloMC/src/CaloDigiMaker_module.cc@1bce0f9bconst int NoiseWFID(0); // will get this from proditions later; const double minAmplitude(2);
minNoiseAmplitudewas afhicl::Atom<float>withminNoiseAmplitude : 2inCaloMC/fcl/prolog.fcl; both the Config entry and the prolog line are deleted in this PR. - Impact: the noise-window threshold can no longer be tuned per campaign without a recompile. The value is unchanged (2), so there is no behaviour change today — this is purely a loss of configurability in the module whose noise model is being reworked.
- Suggested fix: keep
minNoiseAmplitudeas afhicl::Atom(default 2) and restore the prolog line; leave theNoiseWFIDTODO as-is but note it in the PR body so it is not lost.
- Evidence:
-
🟡 [S2]
digiSamplingnow has two independent homes per module- Evidence:
CaloReco/inc/CaloTemplateWFProcessor.hhstill declaresfhicl::Atom<double> digiSamplingandCaloReco/fcl/prolog.fclstill setsdigiSampling : @local::HitMakerDigiSamplingunderTemplateProcessor, but after this PR the processor no longer reads it — the value that actually reachesCaloPulseUtilcomes frompulseCache.digiSampling. The same duplication now exists inCaloDigiMakerandCaloHitTruthMatch(a top-leveldigiSamplingused for the digitisation/time window, pluspulseCache.digiSamplingused to build the pulse cache). - Impact: FHiCL validation still requires the dead
TemplateProcessor.digiSampling, and the two copies can be overridden independently and silently disagree, giving a pulse cache sampled at one rate and a time window computed at another. The prolog sets both from@local::HitMakerDigiSamplingtoday, so nothing is broken now. - Suggested fix: delete the unused
digiSamplingatom fromCaloTemplateWFProcessor::Configand its prolog line; for the modules that legitimately need both, either derive the top-level value frompulseCache.digiSamplingor add a constructor-time consistency check that throws when they differ.
- Evidence:
-
🟡 [S2]
CaloNoiseUtildead / order-dependent state- Evidence:
Mu2eUtilities/inc/CaloNoiseUtil.hhdeclaresdouble minPeakADC_;— it is absent from the constructor initialiser list inCaloNoiseUtil.ccand never read anywhere in the class (it was carried over fromCaloNoiseSimGenerator, where it was used by the now-deletedaddSaltAndPepper). InfillCache,pedestal_is recomputed inside thewhileoverTFilekeys, so with more than one histogram in a batch the surviving value is whichever keyTIteryielded last. - Impact: an uninitialised member that a future edit could read; and a pedestal that is order-dependent on ROOT's key iteration rather than on a defined rule. Neither bites today (one histogram,
minPeakADC_never read). - Suggested fix: delete
minPeakADC_; compute the pedestal from a named histogram (or average across the batch) rather than from whichever key comes last, and say so in the comment. - Related, same file:
noiseSegment()returns astd::span<float>intonoiseMap_, whichfillCache()clears — safe at the single call site inCaloDigiMaker(used immediately), but worth a comment stating that the span must not outlive the nextprepare()/noiseSegment()call.
- Evidence:
-
🟡 [S2] Include guards on the new/renamed
Mu2eUtilitiesheaders do not follow the repo convention- Evidence:
Mu2eUtilities/inc/CaloNoiseUtil.hhusesCaloNoiseUtil_HH;Mu2eUtilities/inc/CaloPulseUtil.hhusesCaloPulseUtil_HH. The convention is path-qualified without the repo prefix, i.e.Mu2eUtilities_CaloNoiseUtil_hh/Mu2eUtilities_CaloPulseUtil_hh. This PR does fixCaloMC/inc/CaloPhotonPropagation.hhtoCaloMC_CaloPhotonPropagation_hh, so the two new headers are inconsistent with the PR's own cleanup. - Impact: collision risk with any future
CaloNoiseUtil/CaloPulseUtilelsewhere in the tree; inconsistency in a package being touched. - Suggested fix: rename both guards to the path-qualified form.
- Evidence:
-
⚪ [S3] Missing and speculative includes in the new files
- Evidence:
Mu2eUtilities/src/CaloNoiseUtil.ccusesstd::istringstreamwithout<sstream>andstd::truncwithout<cmath>(both currently arrive transitively via<iostream>/ROOT headers on this toolchain).Mu2eUtilities/inc/CaloNoiseUtil.hhincludesart/Framework/Services/Optional/RandomNumberGenerator.h,Offline/SeedService/inc/SeedService.hhandfhiclcpp/types/Sequence.h, none of which the header needs — the class only holds CLHEP distributions and afhicl::Atom-basedConfig. These were inherited verbatim fromCaloNoiseSimGenerator.hh. - Impact: violates "include only headers actually needed; avoid speculative includes"; the transitive includes are a toolchain accident, not a guarantee.
- Suggested fix: add
<sstream>and<cmath>to the.cc; drop the three unused includes from the.hh.
- Evidence:
-
⚪ [S3] Small consistency items carried through the refactor
cet::exceptioncategories: the PR improvesCaloDigiMakerto"CALODIGIMAKER"andCaloPhotonPropagationto"CaloPhotonPropagation", but leaves"Rethrow"in all fourShowerStepUtil.ccthrows and"CATEGORY"inCaloPulseUtil.cc; the newCaloNoiseUtil.ccmixes"NOISEREADER","CALONOISEUTIL"and"CaloNoiseUtil"for the same class.- Typo
"Hitsogram"is copied into the renamedCaloPulseUtil.ccand again into new code as"Hitsogram "<<name.c_str()<<" is invalid"inCaloNoiseUtil::fillCache. - Prolog style:
pulseCache : @local::CaloPulseCache(CaloDigiMaker) vspulseCache : { @table::CaloPulseCache }(CaloHitTruthMatch) vs{@table::CaloPulseCache}(TemplateProcessor) — three spellings of the same thing. Mu2eUtilities/CMakeLists.txt:src/CaloPulseUtil.ccis listed beforesrc/CaloNoiseUtil.cc, breaking the otherwise-alphabetical source list.CaloNoiseUtil::pedestal()andprintCache()can beconst.
-
⚪ [S3] PR hygiene
- Best practice reminder: this PR mixes functional changes (noise model,
PEStatCorrectionactivation,.at(),CaloTemplateWFUtilsignature) with a large rename + reformat + diagnostic-histogram removal across 27 files, which is what let finding 1 hide in the diff. Splitting the pure rename/cleanup from the noise-model change would make the physics-relevant delta reviewable on its own. - Best practice reminder: the PR description gives intent but no validation evidence and no acceptance criteria; see finding 4.
- Best practice reminder: this PR mixes functional changes (noise model,
Verified — no action needed
- 🟢 No data-product change. Nothing under
MCDataProducts/orRecoDataProducts/is touched;CaloClusterMC,CaloHitMC,CaloEDepMC,CaloShowerSim,CaloShowerRO,CaloShowerStepkeep their shapes. Consequently noclasses_def.xml/classes.hupdate and no schema-evolution consideration is required for existing files. Checked explicitly because the title suggested otherwise. - 🟢 MC-truth ordering/pairing contracts preserved.
CaloClusterTruthMatchstill sorts itsCaloHitMCvector by descendingtotalEnergyDep()before constructingCaloClusterMC, andCaloHitTruthMatchstill sortsCaloEDepMCby descendingenergyDep(). The rewrite replaces pointer arithmetic (thisCaloCluster - caloClusterBase) with an index loop for theart::Ptr<CaloCluster>— equivalent and safer. No positional cluster-hit ↔ MC-hit pairing is introduced anywhere, so the "CaloClusterMChits are energy-sorted, not positionally aligned" contract still holds. - 🟢
CaloDigiMaker::extract()is behaviour-identical to the deletedCaloWFExtractor::extract(). The old call site constructedCaloWFExtractor(bufferDigi, nBinsPeak, minPeakADC, bufferDigi), i.e.startOffset_ == bufferDigi_, so the inlinedsize_t timeSample(nBinsPeak_+bufferDigi_)reproduces the oldnBinsPeak_+startOffset_exactly; the rest of the body is a verbatim copy. - 🟢
CaloPhotonPropagation::propTimeSimurewrite is equivalent.std::lower_boundover the per-depth CDF returns the same "first bin whose cumulative probability reaches the draw" as the old linearwhile (cdf_[ibin]<test && ibin<iend)scan, with the same clamp tonTimeDiv_-1. The addedizclamp for negativezis a genuine fix (the oldunsigned iz = z/dzTime_was UB forz < 0). Taking ownership of the histogram viastd::unique_ptrfixes a leak. - 🟢
CaloShowerStepMakerancestor logic is preserved and the removed machinery was genuinely dead. Moving thealreadyInspectedlookup ahead of theisInsideAnyCrystal/isInsideSameDisktests yields the same ancestor assignment: sims that now enterinspectedSimsbefore the break map to themselves, which is what the old code's fall-through produced.PhysicalVolumeMultiHelper vi,mapPhysVol_,caloMaterial_andphysVolInfoInput_were write-only —viwas passed intocollectStepBySimAncestorand never referenced in its body, andmapPhysVol_was only ever inserted into. This is a correct dead-state removal. - 🟢
PEStatCorrectionwas a dead knob and is now honoured. At base1ce31dbc,CaloShowerROMaker_module.ccdeclared, initialised and storedPEStatCorrection_at lines 97/111/148 and never read it — Poisson smearing was unconditional. It is now wired into the NPE draw.CaloMC/fcl/prolog.fclsetsPEStatCorrection : trueand no fcl in Offline, Production or mu2e-trig-config overrides it, so default behaviour is unchanged; any config setting itfalsewould now behave differently. - 🟢
deltaTimeMinuspromotion is a genuine fix, value-preserving. The old module had an uninitialiseddouble deltaTimeMinus_member plus a localdouble deltaTimeMin = 100.;next to a// FIXME. The newfhicl::Atom<double> deltaTimeMinusdefaults to100.0, exactly reproducing the old cut, and the uninitialised member is gone. - 🟢 Cross-repo FHiCL contract is clean. Grepped
Offline@1bce0f9b,Production@1e15fc2fandmu2e-trig-config@638f3a5, plus an org-widegh search code: nothing outside the files this PR edits setsNoiseGenerator,addRandomNoise,minNoiseAmplitude,noiseWFSize,nMaxFragment, calopulseFileName/pulseHistName,readoutPEPerMeVCsI/readoutPEPerMeVLyso, orCaloShowerStepMaker.physVolInfoInput/.caloMaterial.Production'sphysVolInfoInputhits are all onStoppedParticlesFinder, a different module. NoCaloPulseShape/CaloNoiseSimGenerator/CaloWFExtractorreference exists outside Offline.EventNtuple,Productionandmu2e-trig-configconsume only module labels (CaloDigiMaker,CaloHitTruthMatch,CaloClusterTruthMatch,CaloShowerStepMaker,CaloShowerROMaker) and product types, none of which are renamed or removed.- One pre-existing stale reference, not caused by this PR:
CaloMC/test/RunCaloCalibGun.fcl:95setsphysics.producers.CaloShowerStepFromStepPt.physVolInfoInput, andCaloShowerStepFromStepPtno longer exists as a module. Worth deleting opportunistically.
- One pre-existing stale reference, not caused by this PR:
- 🟢 Dual build — scons side is correct without edits.
CaloMC/src/SConscript,CaloReco/src/SConscriptandMu2eUtilities/src/SConscriptall exist (93SConscriptfiles remain in the tree) and usehelper.make_mainlib(...)/helper.make_plugins(...), which glob rather than list sources. No*_module.ccis added or removed by this PR — only non-module sources (CaloNoiseSimGenerator.cc,CaloWFExtractor.ccdeleted;CaloPulseShape.cc→CaloPulseUtil.cc;CaloNoiseUtil.ccadded) — so the "new module needs an explicitcet_build_pluginentry that scons picks up for free" asymmetry does not apply here. The CMake source lists inCaloMC/CMakeLists.txtandMu2eUtilities/CMakeLists.txtwere updated correctly for all five files. - 🟢 CMake link dependencies are right.
Offline::ConfigToolsis correctly added to theCaloMCmainlib forCaloPhotonPropagation.cc'sConfigFileLookupPolicy;Mu2eUtilitiesalready listsOffline::ConfigToolsandOffline::SeedService, which is what the newCaloNoiseUtil.ccneeds. TheCaloMC/CaloRecopluginLIBRARIES REGblocks need no additions. - 🟢 CI is green at this exact head.
gh api repos/Mu2e/Offline/commits/1bce0f9b/statusesshowsmu2e/buildtest successplus all 12 art tests success; the FNALbuild comment for1bce0f9b(build 3271, merged into base1ce31dbc) shows ✅ for build (prof), ceSimReco, g4test_03MT, transportOnly, POT, g4study, cosmicSimReco, cosmicOffSpill, ceSteps, ceDigi, muDauSteps, ceMix, rootOverlaps, g4surfaceCheck, trigger, check_cmake, FIXME/TODO (0/0), whitespace. The three failures at the previous headea62775d(ceSimReco, ceDigi, ceMix) are resolved. clang-tidy reports 17 errors / 253 warnings, unchanged in character from main. Note thatcheck_cmakepassing does not cover finding 2 — it validates source lists, not install rules, andbuildtestcompiles with scons.
Re-review / carry-forward
gh api repos/Mu2e/Offline/pulls/1919/reviews→ empty;.../pulls/1919/comments(inline threads) → empty. The only issue comments are FNALbuild CI reports and the author's@FNALbuild run build test. No prior findings from any reviewer to carry forward; this is a first review of the PR.
Validation check
- Build/tests run by CI: yes — scons prof build + 12 art validation jobs + rootOverlaps/g4surfaceCheck/trigger/check_cmake, all green at
1bce0f9b. - Build/tests run by reviewer: partial — reproduced the CMake
install(DIRECTORY)failure standalone (cmake 3.31.8, exit 1). Did not build Offline. - Config contract check: partial — the FHiCL schema is internally coherent and no external repo references a removed key, but
TemplateProcessor.digiSamplingis now a required-but-unused parameter anddigiSamplingis duplicated in three module configs (finding 7). - Cross-repo consistency: pass — no
Production,mu2e-trig-configorEventNtuplechange is required. - CMake / dual-build check: fail — finding 2.
Residual risk
- Finding 1 changes reconstructed calorimeter hit content on every TemplateFit job and is invisible to return-code CI; anything already produced on this branch should be treated as suspect.
- Finding 2 means a CMake-installed release cannot even be produced, and would fail at first event if it were.
- The noise-model swap is only exercised through its
generate: falsehistogram path in CI; thegenerate: truepath (findings 3) has no test coverage at all. CaloNoiseUtilis written for many noise waveforms but only ever driven withNoiseWFID = 0; the multi-batch code path is untested and currently incorrect.
Author follow-ups
- Fix the
CaloTemplateWFUtilconstruction inCaloReco/src/CaloTemplateWFProcessor.ccto passminPeakAmplitude_(finding 1), and consider removing theprintLeveldefault argument so the arity can't silently under-supply again. - Move
install(DIRECTORY data DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/Offline/CaloReco)out ofCaloMC/CMakeLists.txtand intoCaloReco/CMakeLists.txt; verify withcmake --install(not just scons) thatshare/Offline/CaloReco/data/ReadoutNoise.rootlands in the install tree (finding 2). - Fix the double
/basedivision inCaloNoiseUtil::prepare/fillCache/noiseSegment, and decide whetherpeToADCshould key the cache or be removed from the interface (finding 3). - Post before/after pedestal, empty-sample waveform RMS,
CaloHitmultiplicity/energy and reconstructed-peak multiplicity — quantities the RNG re-sequencing does not invalidate — and state howReadoutNoise.rootwas generated (finding 4). - Restore tolerance (or a diagnosable
cet::exception) for aCaloHitwhose crystal has noCaloShowerSim, givenProduction/JobConfig/recoMC/epilog.fclrepointscaloShowerSimCollectionatcompressDigiMCs(finding 5). - Restore
minNoiseAmplitudeas a FHiCL parameter (finding 6); remove the now-unusedTemplateProcessor.digiSamplingand decide on a single home fordigiSampling(finding 7). - Drop the unused
CaloNoiseUtil::minPeakADC_, make the pedestal estimate order-independent, and fix the include guards and includes on the two newMu2eUtilitiesheaders (findings 8–10). - Confirm whether the removal of the
addRandomNoisesalt-and-pepper path is intentional and permanent, or whether it needs to come back in a follow-up — the prolog default wasfalse, so nothing regresses today, but the capability is gone.
michaelmackenzie
left a comment
There was a problem hiding this comment.
This is a large PR, with both logic and code-cleaning changes. Overall it looks fine to me, but I don't think I'll be able to do a detailed review as I'm not a calo reco/MC expert. The trigger results remained stable, which means the reconstruction logic seems stable (there are no random numbers in that CI test, so it shouldn't be affected by changes of that form).
|
@michaelmackenzie If you are not sensitive to the random number sequence, then all changes are cleaning + bug fixes. Since the trigger doesn't change, it means we're in business. |
|
@FNALbuild run build test |
|
⌛ The following tests have been triggered for be443da: build (Build queue - API unavailable) |
|
☔ The build tests failed for be443da.
N.B. These results were obtained from a build of this Pull Request at be443da after being merged into the base branch at 1ce31db. For more information, please check the job page here. |
oksuzian
left a comment
There was a problem hiding this comment.
PR Review Summary — second pass on "Calo mc rev" (#1919)
Reviewed at head be443da3 (2026-08-07). Prior review: 🔴 at 1bce0f9b (review 4884162084, 12 findings). The delta is one commit, be443da3 "Fixed issues and improve structure of noise sampler", +109/−108 across 15 files. Every prior finding is accounted for below and verified in the code at this head rather than taken from the commit message.
Decision
- 🔴 Request changes — one new blocker, and it is the direct cause of the CI failure at this head. Both prior S0s are properly fixed and eight of the twelve findings are closed, several going beyond what I asked. But the fix for my own finding 8 turned
pedestal()from a stored value into a map lookup that throws, and the map is only ever populated as a side effect of a call thataddNoise : falsejobs never make.Production/JobConfig/digitize/OffSpill.fclandNoField.fclboth set exactly that.
Scope understood (delta only)
- Fixes to findings 1, 2, 5, 7, 8, 9, 10, 11;
CaloNoiseUtilrestructured so the pedestal and the waveform cache are keyed per histogram ID;float→doublefor the noise waveform;prepare()reduced to the generate-only path withfillCache()moved to a lazy call insidenoiseSegment().
Findings
-
🔴 [S0]
CaloNoiseUtil::pedestal()throws on everyaddNoise : falsejob — this is thecosmicOffSpillCI failure.- Evidence, the failure:
mu2e/cosmicOffSpillatbe443da3is failure —mu2e -c Production/Validation/cosmicOffSpill.fcl -n 10 finished with return code 1. The same test at1bce0f9bwas success, return code 0, and it is green right now on four other open Offline PRs (#1918, #1916, #1911, #1901), so this is not a brokenmain. The rest of the suite is green at this head;mu2e/buildtestwas re-triggered at 23:15 and is still running. - Evidence, the mechanism —
pedestal_has exactly two writers and one of them is unreachable in this configuration:Mu2eUtilities/src/CaloNoiseUtil.cc:93—pedestal_[hid] = ...insidefillCache()Mu2eUtilities/src/CaloNoiseUtil.cc:134—pedestal_[histoID] = ...insidegenerateCache()fillCache()is called from exactly one place,:152, which is insidenoiseSegment().prepare()is now justif (generate_) generateCache(histoID, peToADC);— with the shipped defaultgenerate : false(CaloReco/fcl/common.fcl,CaloNoiseCache) it does nothing at all.Mu2eUtilities/src/CaloNoiseUtil.cc:168-170—pedestal(histoID)now throwscet::exception("CALONOISEUTIL")when the key is absent, where it previously returned anintmember initialised to 0.
- Evidence, the trigger —
CaloMC/src/CaloDigiMaker_module.cc:199-204:if (isEmpty) continue; if (addNoise_) { const double scaleFactor = readoutScaleFactor(iRO, calCrystalConds); generateSpotNoise(waveform,scaleFactor); // the only path to noiseSegment() } buildOutputDigi(iRO, waveform, noiseSampler_.pedestal(NoiseWFID), caloDigiColl);
Production/JobConfig/digitize/OffSpill.fcl:24setsphysics.producers.CaloDigiMaker.addNoise : false, andProduction/Validation/cosmicOffSpill.fclis a two-line wrapper around that file. With noise off,generateSpotNoisenever runs,noiseSegmentnever runs,fillCachenever runs, and the first non-empty readout of the first event throwsphistoID 0 is invalid for pedestal. - Impact: deterministic, not stochastic — every off-spill digitisation job dies at the first calorimeter readout.
Production/JobConfig/digitize/NoField.fcl:21sets the same key, so no-field digitisation breaks identically; that one has no validation job, so CI would not have told you. There is also a second, data-dependent path withaddNoise : true:generateSpotNoiseonly callsnoiseSegmentfor waveform ranges above its hardcodedminAmplitude = 2, so a job whose first non-empty readout is entirely sub-threshold throws the same way. - Suggested fix: restore eager filling so the cache is valid after
prepare()regardless of source, and callprepare()once per event before the readout loop rather than from inside the noise path —void CaloNoiseUtil::prepare(int histoID, double peToADC) { if (generate_) generateCache(histoID, peToADC); else fillCache(histoID); }
fillCache()clearsnoiseMap_on entry, so calling it once at the top is correct. Guarding the call site (addNoise_ ? noiseSampler_.pedestal(NoiseWFID) : 0.0) fixescosmicOffSpillbut leaves the sub-threshold path in finding 1's second paragraph open, so it is not sufficient on its own. - I could not read
cosmicOffSpill.logto confirm the exception text — build 3272 is still running and its artifacts are not published yet (/artifact/cosmicOffSpill.logreturns Jenkins "Not Found"). The code path above is traced from the source at this head and the config that selects it, so I am confident in the diagnosis, but the log will name the exception if you want it confirmed before patching.
- Evidence, the failure:
-
🟠 [S1]
CaloNoiseUtilgenerate path: the two cache paths key the map differently, andemplacesilently drops the regenerated waveform — carried over from finding 3, now partial.- The double
/basedivision is genuinely fixed:fillCachecomputeshistoBaseID = histoID/baseonce and compareshid/base != histoBaseID(:49,:85). Good. - What remains, all in
generateCache:Mu2eUtilities/src/CaloNoiseUtil.cc:131—noiseMap_.emplace(histoBaseID, waveform);keys by the batch ID, whilenoiseSegmentlooks up by the fullhistoID(:145) andfillCachekeys by the fullhid(:88). The three disagree. ForhistoID = 0they coincide, which is why nothing shows today; for anyhistoID ≥ 1the generate path emplaces at key 0,noiseSegmentmisses, and it throws "noiseSegment called before prepare()".std::map::emplacedoes not overwrite an existing key.CaloDigiMaker::generateSpotNoisecallsprepare(NoiseWFID, scaleFactor)per readout with a per-readoutscaleFactor, so from the second readout on the freshly generated waveform is discarded and the first readout's is reused — the original "peToADC is dead after the first call" problem, in a new form. Worse,pedestal_[histoID]at:134is updated each time, so pedestal and waveform now disagree about which scale factor they were built with.- The discarded regeneration is not free: each call burns one
randPoisson_,nPh×randFlat_, and 10 000 ×randGauss_draws before throwing the result away.
- Impact: latent —
generate : falseis the shipped default, so none of this executes in production or CI. It becomes live the moment per-readout noise IDs arrive from proditions, which is the stated purpose of the class. - Suggested fix: pick one key convention (full
histoIDeverywhere reads simplest, since that is what callers pass) and usenoiseMap_[key] = std::move(waveform)so a regeneration actually replaces. Then either key the cache on(histoID, peToADC)or droppeToADCfrom the interface and document that noise amplitude is absolute ADC.
- The double
-
🟡 [S2] The
.at()fix copies the shower-sim vector on every hit.- Evidence:
CaloMC/src/CaloHitTruthMatch_module.cc:167-169const auto& sortedSimsIt = caloShowerSimsMap.find(hit.crystalID()); const auto& sortedSims = sortedSimsIt == caloShowerSimsMap.end() ? std::vector<const CaloShowerSim*>{} : sortedSimsIt->second;
caloShowerSimsMapisstd::unordered_map<int, std::vector<const CaloShowerSim*>>(:143). Incond ? A : Bthe second operand is a prvalue and the third an lvalue of the same type, so the result is a prvalue:sortedSimsIt->secondis copy-initialised into a temporary.const auto&then binds to that temporary and extends its lifetime — so this is safe, not dangling, but it is a full heap-allocating copy of the vector for everyCaloHit, where the old.at()returned a reference. - Impact: no correctness problem and the crash I flagged is genuinely gone. It is an allocation per hit in the per-event matching loop that reads like a reference, which is the kind of cost that never gets found again.
- Suggested fix: make both operands lvalues so the result stays an lvalue —
(
static const std::vector<const CaloShowerSim*> noSims; const auto it = caloShowerSimsMap.find(hit.crystalID()); const auto& sortedSims = (it == caloShowerSimsMap.end()) ? noSims : it->second;
const auto&on the iterator at:167can also just beconst auto.)
- Evidence:
-
🟡 [S2]
digiSamplingnow has two sources inside one constructor — carried over from finding 7, partial.- The
CaloTemplateWFProcessorhalf is properly fixed: thefhicl::Atom<double> digiSamplingis gone from itsConfigand thedigiSampling : @local::HitMakerDigiSamplingline is gone fromCaloReco/fcl/prolog.fcl. - But
CaloMC/src/CaloDigiMaker_module.ccstill declaresfhicl::Atom<float> digiSampling(:59) and now reads the two differently in adjacent initialisers:Before this delta both came fromdigiSampling_ (config().pulseCache().digiSampling()), // :74 startTimeBuffer_ (config().digiSampling()*config().bufferDigi()), // :76
config().digiSampling()and could not disagree.CaloPulseCache.digiSamplingand the module's owndigiSamplingare both@local::HitMakerDigiSamplingin the prolog, so they agree today — override one andstartTimeBuffer_is computed at one sampling rate whilewaveformSize(:184) uses the other. - Suggested fix: read
config().pulseCache().digiSampling()in both places and delete the top-level atom, or keep the atom and add a constructor check that throws when the two differ.
- The
-
🟡 [S2] Hardcoded knobs — carried over from finding 6, unaddressed, and now duplicated.
CaloMC/src/CaloDigiMaker_module.cc:213still hasconst double minAmplitude(2);; theminNoiseAmplitudefhicl atom and its prolog line remain deleted. Note this constant is not cosmetic after finding 1 — it is what decides whethernoiseSegmentis called at all.const int NoiseWFID(0);is now defined twice, at:181and:211. The:181copy is new in this delta.- Suggested fix: restore
minNoiseAmplitudeas afhicl::Atom(default 2) with its prolog line, and makeNoiseWFIDa single class-scope constant until proditions supplies it.
-
🟡 [S2] No recorded physics evidence — carried over from finding 4, partially addressed; downgraded from 🟠.
- What changed: @michaelmackenzie approved at this exact head, and your comment states that with the trigger unchanged the remaining delta is "cleaning + bug fixes". Sign-off from a reviewer who owns that side is real evidence and I am not going to keep treating this as a major finding.
- What is still missing: nothing quantitative in the PR body. The pedestal definition changed from a closed-form expression to
trunc(mean of histogram bins), andreadoutPEPerMeVCsI/readoutPEPerMeVLysowere deleted, so pedestal value and empty-sample RMS are the two numbers that pin down "same physics" and neither is invalidated by the RNG re-sequencing. - Suggested fix: paste before/after pedestal and empty-sample waveform RMS (and, if cheap,
CaloHitmultiplicity) into the PR body once finding 1 is fixed, plus one line on howReadoutNoise.rootwas produced so it can be regenerated. The PR body is the permanent record; the comment thread is not.
-
⚪ [S3] Small residuals, all new in this delta:
Mu2eUtilities/src/CaloNoiseUtil.cc:172—return iter->second;;(stray second semicolon).:169— the new exception message says"phistoID "for what is ahistoID.CaloMC/src/CaloHitTruthMatch_module.cc:171-176— theforinside the newif (diagLevel_ > 2)block is indented as though it were inside theif (sortedSims.empty())above it; it is not. Behaviour is fine, the indentation is not.noiseSegment()still returns astd::span<double>intonoiseMap_, andfillCache()— now reachable from insidenoiseSegment()— clears that map. Still safe at the single call site, which consumes the span immediately, but the lifetime rule now spans two functions and is worth the one-line comment I suggested last time.- Pre-existing, not from this PR, still there:
CaloMC/test/RunCaloCalibGun.fcl:95setsphysics.producers.CaloShowerStepFromStepPt.physVolInfoInputfor a module that no longer exists.
Carry-forward accounting (vs review 4884162084 at 1bce0f9b)
- 🟢 [was S0]
CaloTemplateWFUtilargument shift — FIXED, verified.CaloReco/src/CaloTemplateWFProcessor.cc:29now passes all four arguments,fmutil_(config.pulseCache(),minPeakAmplitude_,minDTPeaks_,config.fitPrintLevel()), and you took the second half of the suggestion too —CaloTemplateWFUtil.hh:15drops theprintLevel=-1default, so an under-supplied call can no longer compile. I also checked the thing that would have made this fix a fresh bug:minPeakAmplitude_(:82) andminDTPeaks_(:84) are both declared beforefmutil_(:89), so they are initialised before being read. - 🟢 [was S0]
install(DIRECTORY data ...)— FIXED, verified. Removed fromCaloMC/CMakeLists.txt, added toCaloReco/CMakeLists.txt:48, and — as prescribed — noconfigure_file(... ${CURRENT_BINARY_DIR} ...)staging line came with it. - 🟠 [was S1] Noise cache keying — PARTIAL, now finding 2.
- 🟡 [was S1] Physics evidence — PARTIAL, downgraded, now finding 6.
- 🟢 [was S2]
.at()on the shower map — FIXED. No longer throws on aCaloHitwhose crystal has noCaloShowerSim; the unmatched case falls through to an empty vector and adiagLevel_ > 2message. The efficiency of the replacement is finding 3, not a regression of this finding. - 🟡 [was S2] Hardcoded knobs — UNADDRESSED, now finding 5.
- 🟡 [was S2]
digiSamplinghomes — PARTIAL, now finding 4. - 🟢 [was S2]
CaloNoiseUtildead / order-dependent state — FIXED.minPeakADC_is gone from the header; the pedestal is now astd::map<int,double>filled per histogram inside the loop, so it no longer depends on whichTIterkey came last. This fix is also what created finding 1 — the value became unavailable rather than merely wrong. - 🟢 [was S2] Include guards — FIXED.
Mu2eUtilities_CaloNoiseUtil_hhandMu2eUtilities_CaloPulseUtil_hh. - 🟢 [was S3] Includes — FIXED.
<cmath>and<sstream>added to the.cc;Sequence.h,RandomNumberGenerator.handSeedService.hhdropped from the.hh.art_root_io/TFileService.handTFileDirectory.halso came out ofCaloDigiMaker_module.cc. - 🟢 [was S3] Consistency batch — DONE.
"Rethrow"→"SHOWERSTEPUTIL"at all fourShowerStepUtilthrows;"CATEGORY"→"CALOPULSEUTIL";"NOISEREADER"/"CaloNoiseUtil"unified to"CALONOISEUTIL"; both"Hitsogram"typos corrected;Mu2eUtilities/CMakeLists.txtback in alphabetical order;printCache()andpedestal()nowconst;pulseCache : @local::CaloPulseCachespelled the same way in all three places. New nits are in finding 7. - ⚪ [was S3] PR hygiene — partially addressed. The body still carries no validation evidence (finding 6); the single-topic point is water under the bridge at this stage.
Verified 🟢 — no action needed
- 🟢 The
float→doublechange for the noise waveform is harmless.histo->GetArray()returnsconst float*from aTH1F, so the stored values are unchanged, just widened; the cache is 10 000 elements, so the extra 40 kB per waveform is not worth a comment.CaloDigiMaker'swaveformwas alreadystd::vector<double>, so this removes a narrowing at the summation site rather than adding one. - 🟢 Everything cleared in the first review is still clear at this head. No data product changed; MC-truth ordering contracts in
CaloClusterTruthMatch/CaloHitTruthMatchare untouched by this delta;extract(),propTimeSimu, theCaloShowerStepMakerancestor logic,PEStatCorrectionanddeltaTimeMinusare all unmodified since1bce0f9b. - 🟢 Cross-repo FHiCL contract still holds. The only new external coupling this delta creates is the
addNoise : falseinteraction in finding 1, which is a code bug rather than a config-contract break — noProductionormu2e-trig-configfile needs editing onceprepare()is fixed.CaloPulseCache.digiSamplingandCaloDigiMaker.digiSamplingboth resolve to@local::HitMakerDigiSampling, so finding 4 is latent. - 🟢 Dual build still fine. No module added or removed, so the scons glob is unaffected; the CMake source list reorder in
Mu2eUtilities/CMakeLists.txtis cosmetic;CaloReco/CMakeLists.txtgains only the install rule.
Validation check
- Build/tests run by CI: failing at this head —
mu2e/cosmicOffSpillreturns 1 (finding 1). Green:ceDigi,ceMix,ceSimReco,ceSteps,cosmicSimReco,g4study,g4test_03MT,muDauSteps,POT,transportOnly.mu2e/buildtestwas re-triggered at 23:15 and is still pending, so the compile of this head is not yet confirmed by CI. - Build/tests run by reviewer: none this pass. Findings are traced from the source at
be443da3, the CI status API, andProduction@main. I could not retrievecosmicOffSpill.log(build still running). - Config contract check: fail — finding 1 (
addNoise : falseno longer works). - Cross-repo consistency: pass — no
Production/mu2e-trig-configedit is needed; theOffSpill.fclandNoField.fclsettings are correct and it is Offline that must accommodate them. - CMake / dual-build check: pass — the previous S0 is fixed correctly.
Residual risk
NoField.fclsetsaddNoise : falsetoo and has no validation job, so finding 1 breaks no-field digitisation silently — nothing in CI would have caught it ifcosmicOffSpillhad not existed.- The
generate : truepath remains completely untested and, per finding 2, still incorrect for any noise ID other than 0. It is dead code today; it will not be when proditions supplies real IDs. - The pedestal is now a per-histogram map with no default, which is the right model, but every consumer of it is one missed
prepare()away from an exception rather than a wrong number. Worth deciding deliberately whether "no noise configured" should mean pedestal 0 or a hard error.
Author follow-ups
- Fix
CaloNoiseUtil::prepare()to fill the cache in the non-generate case, and call it once per event outside theaddNoise_guard — then confirmcosmicOffSpillgoes green (finding 1, blocking). - Make
generateCachekeynoiseMap_the same wayfillCacheandnoiseSegmentdo, and replace rather thanemplace(finding 2). - Avoid the per-hit vector copy in
CaloHitTruthMatch(finding 3). - Settle
digiSamplingon one source insideCaloDigiMaker(finding 4); restoreminNoiseAmplitudeand de-duplicateNoiseWFID(finding 5). - Put the pedestal / empty-sample-RMS before-and-after in the PR body, plus one line on how
ReadoutNoise.rootwas made (finding 6). - The nits in finding 7 whenever you next touch these files.
|
There was a stupid typo that made it through, dammit... Will fix this later. |
|
@FNALbuild run build test |
|
⌛ The following tests have been triggered for 8d9ef25: build (Build queue - API unavailable) |
|
☀️ The build tests passed at 8d9ef25.
N.B. These results were obtained from a build of this Pull Request at 8d9ef25 after being merged into the base branch at 99536c1. For more information, please check the job page here. |
oksuzian
left a comment
There was a problem hiding this comment.
PR Review Summary — third pass on "Calo mc rev" (#1919)
Reviewed at head 8d9ef253. Prior reviews: 🔴 at 1bce0f9b (4884162084, 12 findings), 🔴 at be443da3 (4887293882, 7 findings). The delta is one commit, 8d9ef253 "Further bug fixes", +48/−41 across 6 files. Every prior finding is accounted for below and verified in the source at this head, not taken from the commit message.
Decision
- 🟡 Comment only. Upgraded from 🔴.
The S0 is properly fixed — both halves of it, including the sub-threshold sub-case that CI would not have caught — and the S1 keying bug is fixed. What is left is one debug std::cout that is running in production CI logs right now, and a handful of carried-forward minors. None of it gates.
Scope understood (delta only)
CaloNoiseUtil::prepare()regains theelse fillCache(histoID)branch;noiseSegment()loses its lazy-fill fallback;generateCachekeysnoiseMap_by fullhistoIDand assigns rather thanemplaces; a newpeToADC_map guards regeneration at a different scale.CaloDigiMaker:minNoiseAmprestored as a FHiCL atom,digiSamplingatom removed,NoiseWFIDde-duplicated,generateSpotNoiserenamedAddWFNoisewithprepare()hoisted to the call site, and the pedestal defaulted to 0 whenaddNoise_is false.
Carry-forward accounting (vs review 4887293882 at be443da3)
1. 🟢 [was S0] pedestal() throws on every addNoise : false job — FIXED in 8d9ef253, verified.
You took both halves of the prescription, and the second half is what actually matters.
prepare()(Mu2eUtilities/src/CaloNoiseUtil.cc:42-46) now fills the cache in the non-generate case, exactly as suggested.prepare()is hoisted out of the noise-adding function to the call site (CaloMC/src/CaloDigiMaker_module.cc:204), where it runs unconditionally inside theaddNoise_block beforeAddWFNoise. This closes the second, data-dependent failure I flagged — theaddNoise : truejob whose first non-empty readout is entirely sub-threshold. Under the old structureprepare()sat behind theminAmplituderange scan and could be skipped; it no longer can. That path has no CI coverage, so a green suite would not have told you it was fixed.- The
addNoise : falsepedestal is now a literal0(:200), and I checked this against the pre-PR behaviour rather than assuming:CaloNoiseSimGeneratorinitialisedpedestal_(0.0)in its constructor (CaloMC/src/CaloNoiseSimGenerator.cc:26at base1ce31dbc) andinitialize()was only called underif (addNoise_). So base also digitized with pedestal 0 when noise was off. Value-preserving, not merely non-throwing.
Verified downstream: mu2e/cosmicOffSpill is green at this head (build 3279), as are all 12 art jobs, buildtest, rootOverlaps, g4surfaceCheck, trigger and check_cmake. Production/JobConfig/digitize/NoField.fcl:23 sets the same addNoise : false and has no validation job; it is fixed by the same change.
2. 🟢 [was S1] generateCache keying and emplace — FIXED, verified.
All three sites now agree on the full histoID: noiseMap_[histoID] = std::move(waveform) (:145), noiseMap_[hid].assign(...) (:92), noiseMap_.find(histoID) (:161). The batch ID survives only where it belongs, as the std::erase_if filter (:118) and the fillCache batch test (:89). emplace → assignment means a regeneration now actually replaces. The noiseSegment-before-prepare throw is also gone, correctly — with prepare() hoisted it is now unreachable by construction.
The peToADC half became a design decision rather than the fix I suggested; see finding 2 below. Not a regression, so I am closing this one.
3. 🟢 [was S2] Per-hit vector copy in CaloHitTruthMatch — FIXED, verified.
CaloMC/src/CaloHitTruthMatch_module.cc:167-169 is now the static const + lvalue-ternary form, so sortedSims binds to a reference and the per-hit heap allocation is gone. The stray indentation in the adjacent diagLevel_ > 2 block is fixed too.
4. 🟡 [was S2] digiSampling two homes — PARTIAL. CaloDigiMaker is fully fixed: the atom is deleted, and both digiSampling_ (:74) and startTimeBuffer_ (:76) now read config().pulseCache().digiSampling(), so they cannot disagree. CaloHitTruthMatch still has the problem; see finding 3.
5. 🟢 [was S2] Hardcoded knobs — FIXED, verified. minNoiseAmp is back as fhicl::Atom<float> (:59) with minNoiseAmp : 2 in CaloMC/fcl/prolog.fcl:56, and it is read at both comparison sites in AddWFNoise. NoiseWFID is now defined once (:202) and passed as a parameter. I confirmed no file in Offline, Production or mu2e-trig-config overrides the removed CaloDigiMaker.digiSampling, so deleting a required atom breaks no external config.
6. 🟡 [was S2] Physics evidence — UNADDRESSED, carried over. Still nothing quantitative in the PR body. See finding 4.
7. ⚪ [was S3] Small residuals — MOSTLY FIXED. The return iter->second;; double semicolon and the "phistoID" message are corrected. CaloMC/test/RunCaloCalibGun.fcl drops the stale CaloShowerStepFromStepPt.physVolInfoInput line — thanks for taking that one, it was pre-existing and not yours. The noiseSegment span-lifetime comment did not appear; new nits in finding 5.
Findings
1. 🟡 [S2] Two debug std::cout statements ship enabled — they are in the CI logs at this head.
- Evidence:
Mu2eUtilities/src/CaloNoiseUtil.cc:54and:115, both unindented at column 0:Not speculative — I pulled the CI artifact for this head and grepped it.std::cout<<"Fill Cache noise\n"; std::cout<<"Generate Cache noise\n";
ceDigi.logfrom build 3279 containsFill Cache noiseat line 52, interleaved with the art record banners:Begin processing the 3rd record. run: 1430 subRun: 0 event: 6 ... Fill Cache noise Begin processing the 5th record. run: 1430 subRun: 0 event: 9 ... - Impact: bounded but real. It is one line per job, not per event — which is itself useful confirmation that your new
fillCacheearly-return works — so this is log noise, not a performance problem. It violates "protect production prints with a verbosity flag or message facility", and the column-0 indentation makes clear these were temporary.cosmicOffSpill.loghas zero occurrences, which is theaddNoise : falsepath correctly never filling the cache. - Suggested fix: delete both, or gate them behind the existing diagnostic level.
2. 🟡 [S2] The new peToADC guard turns a silent-wrong-answer into a job abort, and the call site can still trigger it.
- Evidence:
Mu2eUtilities/src/CaloNoiseUtil.cc:109-113throws when the samehistoIDis prepared with apeToADCdiffering by ≥ 0.01. The only caller passes a per-SiPM scale factor against a fixedhistoID = 0(CaloDigiMaker_module.cc:202-204), andreadoutScaleFactorreturnsADCPerMeV/pePerMeVfor that specific SiPM (:360-367). - Whether it fires, checked rather than assumed:
CaloConditions/data/Sim_crystal.txthas 1348 crystals with exactly two distinct ratios —0.533333(1344 crystals) and0.534000(4 crystals, e.g. crystal 582 with500 500 267 267). Max spread is6.7e-4, 15× below the 0.01 tolerance. So withgenerate : falseshipped as the default this is dead code, and even withgenerate : trueit does not throw on the current conditions file. - Impact: latent, and I want to be precise about what changed. Previously a differing scale factor was silently ignored (the
emplaceno-op). Now it is a hard error. That is the better failure mode, but the tolerance is an absolute cut on a dimensionless ratio whose value happens to be ~0.53; a future conditions file with genuinely per-crystal calibration would abort the job rather than produce per-crystal noise. The guard encodes "one noise waveform for the whole detector" as an invariant while the surrounding code is being built for per-readout IDs. - Suggested fix: no action needed for this PR. When proditions supplies real noise IDs, either key the cache on
(histoID, peToADC)so different scales coexist, or droppeToADCfrom the interface and document noise as absolute ADC. Worth a// TODOnext to the throw so the tolerance is not mistaken for a physics number.
3. 🟡 [S2] CaloHitTruthMatch.digiSampling is now required-but-never-read — carried over from finding 4, partial.
- Evidence:
CaloMC/src/CaloHitTruthMatch_module.cc:42still declaresfhicl::Atom<double> digiSampling, andCaloMC/fcl/prolog.fcl:68still sets it. But:56initialisesdigiSampling_(config().pulseCache().digiSampling())— I grepped forconfig().digiSampling()in that file and there are zero reads. FHiCL validation still rejects a config that omits the key, and overriding it silently does nothing. - Impact: no behaviour change today. It is the same trap you just closed in
CaloDigiMaker, left open in the sibling module: a knob that looks live and is not. - Suggested fix: delete the atom and the prolog line, exactly as you did for
CaloTemplateWFProcessorin the previous round.
4. 🟡 [S2] Still no recorded physics evidence — carried over.
- The approval from @michaelmackenzie plus stable trigger results remains the strongest evidence on this PR, and I am not re-raising the severity. But the PR body is the permanent record and it still says only "the physics remains the same" with no numbers, for a change that replaced the noise source and redefined the pedestal from a closed-form expression to
trunc(mean of histogram bins). - Suggested fix: paste before/after pedestal and empty-sample waveform RMS into the body — neither is invalidated by the RNG re-sequencing — plus one line on how
ReadoutNoise.rootwas produced. The file contains a singlehisto_0, so the regeneration recipe is the only way anyone reproduces it later.
5. ⚪ [S3] Nits, all new in this delta.
- Typos in new comments/messages:
"alredy"(:107),"differnt"in a user-visible exception string (:112),"histogrm"(:87). AddWFNoiseis the only PascalCase method among nine inCaloDigiMaker(makeDigitization,fillROHits,buildOutputDigi,extract,readoutScaleFactor,diag0,diag1). Its parameter isnoiseWFIDin the declaration (:105) andNoiseWFIDin the definition (:215).CaloNoiseUtil::pedestal()returnsintwhilepedestal_isstd::map<int,double>(CaloNoiseUtil.hh:43,:63) — thedoubleis truncated a second time on return. Harmless because both writers alreadystd::trunc, but the signature says the map should beint, or the return should bedouble.- The
noiseSegmentspan-lifetime comment I suggested twice is still absent. Lower stakes now thatfillCacheis no longer reachable from insidenoiseSegment, so this is the last time I raise it.
🟢 Verified — no action needed
- 🟢
addNoise : falseis value-preserving, not just non-throwing — checked againstCaloNoiseSimGenerator's constructor at base rather than inferred from the green CI. - 🟢 The S0 sub-threshold sub-case is closed by construction, not by luck —
prepare()now precedes the range scan. - 🟢 Removing the
CaloDigiMaker.digiSamplingatom breaks no external config. Searched Offline,Production@20c93edcandmu2e-trig-config@51b30e69, plus an org-wide code search: the only externalCaloDigiMakeroverrides anywhere are the twoaddNoise : falselines inProduction/JobConfig/digitize/{OffSpill,NoField}.fcl. Removing a required FHiCL atom is the one refactor that breaks callers silently at job start, so this was worth confirming explicitly. - 🟢
fillCache's new early-return is correct in all three cases — same ID cached (returns), different ID same batch (reloads batch), different batch (clears, loads new batch). The one-print-per-job inceDigi.logis direct evidence of the first case working. - 🟢 CI fully green at this exact head (
8d9ef253, build 3279, merged into base99536c11):buildtest, all 12 art jobs including the previously-failingcosmicOffSpill,rootOverlaps,g4surfaceCheck,trigger,check_cmake, FIXME/TODO 0/0, whitespace clean. clang-tidy 17 errors / 249 warnings, unchanged in character frommain. I did not re-trigger — the head is already green. - 🟢 Everything cleared in passes one and two still holds. No data product changed; MC-truth ordering contracts intact;
extract(),propTimeSimu,CaloShowerStepMakerancestor logic,PEStatCorrection,deltaTimeMinusuntouched since1bce0f9b. TheCaloReco/CMakeLists.txtinstall rule from pass two is unchanged, so the CMake S0 stays fixed.
Validation check
- Build/tests run by CI: yes, all green at
8d9ef253. - Build/tests run by reviewer: none. Findings traced from source at this head, the shipped
Sim_crystal.txt, the base revision1ce31dbc, and the fetchedceDigi.log/cosmicOffSpill.logartifacts from build 3279. - Config contract check: pass —
addNoise : falseworks again; no external override of any removed atom. One dead-but-required key remains (finding 3). - Cross-repo consistency: pass — no
Productionormu2e-trig-configedit needed. - CMake / dual-build check: pass — unchanged since the pass-two fix.
Residual risk
- The
generate : truepath is still entirely untested. It is closer to correct than it has been — the keying is fixed and the scale-factor collision is now loud — but nothing exercises it. - The pedestal remains a per-histogram map with no default. Correct model, and the call site now guarantees
prepare()runs first, but any future caller that forgets gets an exception rather than a wrong number. That is the right trade; worth keeping in mind when proditions wiring lands. - Physics evidence for the noise-model swap still rests on the trigger-stability argument rather than on pedestal/RMS numbers.
Author follow-ups (none blocking)
- Delete the two
std::coutdebug lines inCaloNoiseUtil.cc:54,115— they are in the CI logs at this head (finding 1). - Drop the unread
digiSamplingatom fromCaloHitTruthMatchand its prolog line, mirroring theCaloDigiMakerfix you already made (finding 3). - Put pedestal and empty-sample-RMS before/after in the PR body, plus one line on how
ReadoutNoise.rootwas generated (finding 4). - The nits in finding 5 whenever you next touch these files; a
// TODOon thepeToADCtolerance (finding 2) would be worth adding while the reasoning is fresh.
Both blockers I raised across two passes are now genuinely fixed, and in each case you implemented the part that mattered rather than the part that would have made CI green — hoisting prepare() closed a sub-threshold path that no test covers. My remaining findings are all things that can ride along with the next touch of these files.
oksuzian
left a comment
There was a problem hiding this comment.
PR Review Summary — approval on #1919, "Calo mc rev"
Reviewed at head 8d9ef253. This supersedes the decision on my third-pass review (4890573928) posted minutes ago at this same head — the findings there stand exactly as written, and I am not repeating them here.
Decision
- 🟢 Approve. Supersedes my 🔴 at
1bce0f9b(4884162084) and 🔴 atbe443da3(4887293882).
Why this is a separate post
I posted the third pass as a comment-only review. That was the wrong event: a COMMENTED review does not clear a prior CHANGES_REQUESTED, so my two earlier blocking reviews were still gating this PR while the review body said nothing remaining blocks. That is contradictory, and only I can lift it. Correcting it here.
What this approval rests on — all verified at 8d9ef253 in the third-pass review:
- 🟢 [was S0]
pedestal()throwing on everyaddNoise : falsejob — fixed.prepare()regained its non-generate branch and was hoisted to the call site ahead of the range scan, which also closes the sub-thresholdaddNoise : truepath that no CI test covers. TheaddNoise : falsepedestal of0is value-preserving against base1ce31dbc, not merely non-throwing. - 🟢 [was S0]
install(DIRECTORY data ...)in the wrong package — fixed in the previous round and unchanged since. - 🟢 [was S1]
CaloTemplateWFUtilargument shift — fixed in the previous round, with theprintLeveldefault dropped so the arity cannot silently under-supply again. - 🟢 [was S1]
generateCachekeying andemplace— fixed. All three sites key on the fullhistoID; regeneration now replaces. - 🟢 CI fully green at this head (build 3279), including the previously-failing
cosmicOffSpill.
Non-blocking follow-ups — carried from review 4890573928, none gating:
- Delete the two debug
std::coutlines inMu2eUtilities/src/CaloNoiseUtil.cc:54,115. These are live in the CI logs at this head (one line per job inceDigi.log), so they will ship into production logs. This is the one I would genuinely like to see gone, and it is a two-line deletion. - Drop the unread
digiSamplingatom fromCaloHitTruthMatchand its prolog line, mirroring the fix you already made inCaloDigiMaker. - Add pedestal and empty-sample-RMS before/after to the PR body, plus one line on how
ReadoutNoise.rootwas generated. - Typos (
alredy,differnt,histogrm),AddWFNoisecasing,pedestal()returningintfrom adoublemap, and a// TODOon thepeToADCtolerance.
Approving rather than holding the PR for these: the two real blockers are fixed and verified, @michaelmackenzie has approved the reconstruction side, and none of the above changes behaviour. Item 1 is worth doing before merge if it is convenient, but I do not think it warrants blocking.
|
I'll make another PR to address the last nits, I like when this is clean |
Refactoring and modernizing the CaloMC code and fixing a few bugs. The new noise sampler class moved to CaloReco since it will also be needed for reco. The new noise model let you use the waveform taken from histograms, which is what will be needed to include conditions. It keeps the old method to generate noise waveform from scratch if needed. Switched to noise histogram readout to speed up start, which invalidates the RNG sequence so the validation should fail (tested that the validation is ok with the older version and all other changes), but the physics remains the same. Noise file generated with parameters found in fcl file. Check that noise mean/RMS is same before/after (not posting details here)