From a8a5c66261adc536807b3e932914207fccaa333d Mon Sep 17 00:00:00 2001 From: VCLanNguyen Date: Fri, 4 Sep 2026 18:11:41 -0500 Subject: [PATCH 01/15] Extend SBND data-stream CRT/PMT reference-time shift to opt0_sec, barycenterFM, fmatch variants, and CRUMBS features Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Lxqnx6r8HqPPywzXcxBahz --- sbncode/CAFMaker/CAFMaker_module.cc | 33 +++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/sbncode/CAFMaker/CAFMaker_module.cc b/sbncode/CAFMaker/CAFMaker_module.cc index b38ed1f6a..bcb12bf31 100644 --- a/sbncode/CAFMaker/CAFMaker_module.cc +++ b/sbncode/CAFMaker/CAFMaker_module.cc @@ -543,7 +543,13 @@ void CAFMaker::SBNDShiftCRTReference(StandardRecord &rec, double SBNDFrame) cons if(!std::isnan(pfp.trk.crtsbndtrack.score)) pfp.trk.crtsbndtrack.track.time += SBNDFrame; } + + // CRUMBS CRT input features [us] + slc.crumbs_result.crt.tracktime += SBNDFrame / 1000.; + slc.crumbs_result.crt.sptime += SBNDFrame / 1000.; } + + // TODO: SRSBNDCRTVeto.sp_time (FillReco.cxx) is not shifted here -- watch list. } void CAFMaker::SBNDShiftPMTReference(StandardRecord &rec, double SBNDFrame) const { @@ -559,7 +565,22 @@ void CAFMaker::SBNDShiftPMTReference(StandardRecord &rec, double SBNDFrame) cons //OpT0 match to slice for (SRSlice &s: rec.slc) { s.opt0.time += SBNDFrame_us; + s.opt0_sec.time += SBNDFrame_us; + + s.barycenterFM.flashTime += SBNDFrame_us; + s.barycenterFM.flashFirstHit += SBNDFrame_us; + + s.fmatch.time += SBNDFrame_us; + s.fmatchop.time += SBNDFrame_us; + s.fmatchara.time += SBNDFrame_us; + s.fmatchopara.time += SBNDFrame_us; + + s.crumbs_result.pds.fmtime += SBNDFrame_us; } + + // TODO: SRCorrectedOpFlash (slice.correctedOpFlash) not yet shifted here; see + // https://github.com/SBNSoftware/sbncode/pull/668#pullrequestreview-5117853839 + // TODO: SRSoftwareTrigger.flash_peaktime not yet shifted here. } void CAFMaker::FixPMTReferenceTimes(StandardRecord &rec, double PMT_reference_time) { @@ -2696,9 +2717,9 @@ void CAFMaker::produce(art::Event& evt) noexcept { // TODO: TPC? - // SBND: Fix the Reference time in data depending on the stream - // For more information, see: - // https://sbn-docdb.fnal.gov/cgi-bin/sso/RetrieveFile?docid=43090 + // SBND: Fix the reference time in data exclusively for Gen2 Sample (v10_14_02) + // For information, see: https://sbn-docdb.fnal.gov/cgi-bin/sso/RetrieveFile?docid=43090 + // Otherwise, this code block is legacy for sbncode > v10_20_09 if (isRealData && (fDet == kSBND) && fSubRunPOT > 0) { @@ -2709,11 +2730,11 @@ void CAFMaker::produce(art::Event& evt) noexcept { if (!std::isnan(rec.sbnd_frames.frameApplyAtCaf) && (rec.sbnd_frames.frameApplyAtCaf != 0.0)){ mf::LogInfo("CAFMaker") << "Setting Reference Timing for timing object in SBND \n" << " Shift Apply At Caf Level = " << rec.sbnd_frames.frameApplyAtCaf << " ns\n"; - - //shift reference frame for CRT objects: crt trk, crt sp, crt sp match, crt trk match + + //shift reference frame for CRT objects: crt trk, crt sp, crt sp match, crt trk match, CRUMBS CRT features SBNDShiftCRTReference(rec, rec.sbnd_frames.frameApplyAtCaf); - //shift reference frame for PMT objects: opflash, opt0 + //shift reference frame for PMT objects: opflash, opt0/opt0_sec, barycenter flash match, simple flash matches, CRUMBS PDS feature SBNDShiftPMTReference(rec, rec.sbnd_frames.frameApplyAtCaf); } } From 28ff628a04c8b8c7e846f3004d0854ea5d1b9a4f Mon Sep 17 00:00:00 2001 From: VCLanNguyen Date: Fri, 4 Sep 2026 18:15:01 -0500 Subject: [PATCH 02/15] Fill SRTrueInteraction.prod_time and .dk2gen from dk2nu/MCFlux in CAFMaker Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Lxqnx6r8HqPPywzXcxBahz --- sbncode/CAFMaker/CMakeLists.txt | 1 + sbncode/CAFMaker/FillTrue.cxx | 6 ++++++ sbncode/CAFMaker/FillTrue.h | 3 +++ 3 files changed, 10 insertions(+) diff --git a/sbncode/CAFMaker/CMakeLists.txt b/sbncode/CAFMaker/CMakeLists.txt index 7a5162082..a53793dd5 100644 --- a/sbncode/CAFMaker/CMakeLists.txt +++ b/sbncode/CAFMaker/CMakeLists.txt @@ -23,6 +23,7 @@ art_make_library( LIBRARY_NAME sbncode_CAFMaker ROOT::EG CLHEP::Random caf_RecoUtils + dk2nu::Tree lardataobj::AnalysisBase lardataobj::RecoBase lardataobj::Simulation diff --git a/sbncode/CAFMaker/FillTrue.cxx b/sbncode/CAFMaker/FillTrue.cxx index 4d1d794d0..9d291b5fe 100644 --- a/sbncode/CAFMaker/FillTrue.cxx +++ b/sbncode/CAFMaker/FillTrue.cxx @@ -1,5 +1,7 @@ #include "FillTrue.h" +#include "dk2nu/tree/dk2nu.h" + #include "larcorealg/GeoAlgo/GeoAlgo.h" #include "larcorealg/Geometry/GeometryCore.h" #include "larcorealg/Geometry/WireReadoutGeom.h" @@ -457,6 +459,7 @@ namespace caf { //------------------------------------------------ void FillTrueNeutrino(const art::Ptr mctruth, const simb::MCFlux &mcflux, + const bsim::Dk2Nu &dk2nu, const simb::GTruth& gtruth, const std::vector &srparticles, const std::map>> &id_to_truehit_map, @@ -549,6 +552,9 @@ namespace caf { // Set the MCFlux stuff srneutrino.initpdg = mcflux.fntype; srneutrino.baseline = mcflux.fdk2gen + mcflux.fgen2vtx; + srneutrino.dk2gen = mcflux.fdk2gen; + srneutrino.prod_time = (!dk2nu.ancestor.empty()) + ? dk2nu.ancestor.back().startt : std::numeric_limits::signaling_NaN(); srneutrino.parent_pdg = mcflux.fptype; srneutrino.parent_dcy_mode = mcflux.fndecay; srneutrino.prod_vtx.x = mcflux.fvx; diff --git a/sbncode/CAFMaker/FillTrue.h b/sbncode/CAFMaker/FillTrue.h index 419ed6ee9..e7579d799 100644 --- a/sbncode/CAFMaker/FillTrue.h +++ b/sbncode/CAFMaker/FillTrue.h @@ -33,6 +33,8 @@ #include "sbnanaobj/StandardRecord/StandardRecord.h" #include "sbnanaobj/StandardRecord/SRMeVPrtl.h" +namespace bsim { class Dk2Nu; } + namespace caf { struct HitsEnergy { @@ -86,6 +88,7 @@ namespace caf void FillTrueNeutrino(const art::Ptr mctruth, const simb::MCFlux &mcflux, + const bsim::Dk2Nu &dk2nu, const simb::GTruth& gtruth, const std::vector &srparticles, const std::map>> &id_to_truehit_map, From 865a4b15622d620c78d2b7067aa6dbd4286b1fb4 Mon Sep 17 00:00:00 2001 From: VCLanNguyen Date: Fri, 4 Sep 2026 18:20:54 -0500 Subject: [PATCH 03/15] Correct dk2nu v01_11_00 vertex-time bug for SBND MC truth and slice-scoped reco objects Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Lxqnx6r8HqPPywzXcxBahz --- sbncode/CAFMaker/CAFMaker_module.cc | 111 +++++++++++++++++++++++++++- 1 file changed, 110 insertions(+), 1 deletion(-) diff --git a/sbncode/CAFMaker/CAFMaker_module.cc b/sbncode/CAFMaker/CAFMaker_module.cc index bcb12bf31..6dc8560a9 100644 --- a/sbncode/CAFMaker/CAFMaker_module.cc +++ b/sbncode/CAFMaker/CAFMaker_module.cc @@ -42,6 +42,8 @@ #include "ifdh_art/IFDHService/IFDH_service.h" +#include "dk2nu/tree/dk2nu.h" + // ROOT includes #include "TFile.h" #include "TH1D.h" @@ -338,6 +340,8 @@ class CAFMaker : public art::EDProducer { void SBNDShiftCRTReference(StandardRecord &rec, double SBNDFrame) const; void SBNDShiftPMTReference(StandardRecord &rec, double SBNDFrame) const; + void CorrectMCTiming(StandardRecord &rec) const; + /// Equivalent of FindManyP except a return that is !isValid() prints a /// messsage and aborts if StrictMode is true. template @@ -552,6 +556,94 @@ void CAFMaker::SBNDShiftCRTReference(StandardRecord &rec, double SBNDFrame) cons // TODO: SRSBNDCRTVeto.sp_time (FillReco.cxx) is not shifted here -- watch list. } +void CAFMaker::CorrectMCTiming(StandardRecord &rec) const { + + // Recovers the dk2nu v01_11_00 SetT(0) bug (fixed in v01_12_00) + // by adding the missing meson decay time + neutrino tof from meson decay to flux window + constexpr double kSpeedOfLight = 29.9792458; // cm/ns + + // TODO: Does not correct TPC timing variables yet: + // SRCaloPoint.t, SRHit.peakTime, SRPFP.t0, + // SRBlip.time/.timeTick, + // SRBlipHitClust.time/.startTime/.endTime/.timespan/.timeTick + + // Per-neutrino offset, indexed the same as rec.mc.nu / SRSlice.tmatch.index. + std::vector offset_ns(rec.mc.nu.size(), std::numeric_limits::signaling_NaN()); + + for (unsigned i = 0; i < rec.mc.nu.size(); i++) { + SRTrueInteraction &nu = rec.mc.nu[i]; + if (std::isnan(nu.prod_time)) continue; // no dk2nu info for this sample/interaction + + offset_ns[i] = nu.prod_time + nu.dk2gen * 100. / kSpeedOfLight; + double const delta_us = offset_ns[i] / 1000.; + + nu.time += delta_us; + + for (SRTrueParticle &part: nu.prim) { + if (part.genT > -9998.) part.genT += delta_us; + if (part.startT > -9998.) part.startT += delta_us; + if (part.endT > -9998.) part.endT += delta_us; + } + } + + // Apply the matched neutrino's own offset to slice-scoped reco objects (and the + // slice's stale truth copy). SRSlice.tmatch.index (set by MatchSlice2Truth, + // purity/charge-based, independent of nu.time) indexes directly into rec.mc.nu. + // Unmatched/cosmic slices (tmatch.index < 0) are left untouched. + for (SRSlice &slc: rec.slc) { + if (slc.tmatch.index < 0 || std::isnan(offset_ns[slc.tmatch.index])) continue; + + double const off_ns = offset_ns[slc.tmatch.index]; + double const delta_us = off_ns / 1000.; + + // slc.truth is a copy of rec.mc.nu[tmatch.index] taken before this function + // runs (FillTrue.cxx:372) -- needs the same correction applied separately. + slc.truth.time += delta_us; + for (SRTrueParticle &part: slc.truth.prim) { + if (part.genT > -9998.) part.genT += delta_us; + if (part.startT > -9998.) part.startT += delta_us; + if (part.endT > -9998.) part.endT += delta_us; + } + + slc.opt0.time += delta_us; + slc.opt0_sec.time += delta_us; + + slc.barycenterFM.flashTime += delta_us; + slc.barycenterFM.flashFirstHit += delta_us; + + // PMT/XARAPUCA simple flash matches (fmatch=PMT-SimpleFlash, fmatchop=PMT-OpFlash, + // fmatchara=XARAPUCA-SimpleFlash, fmatchopara=XARAPUCA-OpFlash) + slc.fmatch.time += delta_us; + slc.fmatchop.time += delta_us; + slc.fmatchara.time += delta_us; + slc.fmatchopara.time += delta_us; + + slc.crumbs_result.pds.fmtime += delta_us; + slc.crumbs_result.crt.tracktime += delta_us; + slc.crumbs_result.crt.sptime += delta_us; + + for (SRPFP &pfp: slc.reco.pfp) { + if (!std::isnan(pfp.trk.crtspacepoint.score)) pfp.trk.crtspacepoint.spacepoint.time += off_ns; + if (!std::isnan(pfp.trk.crtsbndtrack.score)) pfp.trk.crtsbndtrack.track.time += off_ns; + } + } + + // TODO: watch list -- no truth-matching path currently exists to assign a + // per-object offset to these EVENT-GLOBAL (not slice-scoped) collections: + // rec.opflashes -- would need a PhotonBackTracker-based match; none + // exists in SBND today (OpDetBacktrackerRecords are + // dropped from reco output; PhotonBackTracker unused) + // rec.crt_spacepoints, rec.sbnd_crt_tracks + // -- CRTBackTrackerAlg exists (sbndcode CRT/CRTBackTracker/ + // CRTBackTrackerAlg.{h,cc}) but is only invoked from the + // CRTAnalysis analyzer, not persisted as an art product; + // CAFMaker would need to call it directly (not done here) + // SRSBNDCRTVeto.sp_time + // SRCorrectedOpFlash (slice.correctedOpFlash); see + // https://github.com/SBNSoftware/sbncode/pull/668#pullrequestreview-5117853839 + // SRSoftwareTrigger.flash_peaktime +} + void CAFMaker::SBNDShiftPMTReference(StandardRecord &rec, double SBNDFrame) const { double SBNDFrame_us = SBNDFrame / 1000.0; //convert ns to us @@ -1475,6 +1567,14 @@ void CAFMaker::produce(art::Event& evt) noexcept { art::fill_ptr_vector(mcfluxes, mcflux_handle); } + art::Handle> dk2nu_handle; + GetByLabelStrict(evt, std::string("generator"), dk2nu_handle); + + std::vector> dk2nus; + if (dk2nu_handle.isValid()) { + art::fill_ptr_vector(dk2nus, dk2nu_handle); + } + // get the MCReco for the fake-reco art::Handle> mctrack_handle; GetByLabelStrict(evt, std::string("mcreco"), mctrack_handle); @@ -1558,9 +1658,13 @@ void CAFMaker::produce(art::Event& evt) noexcept { // holder for invalid MCFlux simb::MCFlux badflux; // default constructor gives nonsense values + // holder for invalid Dk2Nu (default-constructed ancestor vector is empty -- treated as "absent") + bsim::Dk2Nu baddk2nu; + for (size_t i=0; i Date: Fri, 25 Sep 2026 12:33:02 -0500 Subject: [PATCH 04/15] Guard MC/data timing shifts against unfilled sentinel defaults Fields shifted by CorrectMCTiming()/SBNDShiftCRTReference()/SBNDShiftPMTReference()/ FixPMTReferenceTimes() came from optional slice-scoped associations (OpT0Finder candidate, TPCPMTBarycenterMatch, SimpleFlashMatch, CRUMBSResult) that aren't guaranteed to exist for a given slice. Shifting an absent association's field turned its honest "not filled" sentinel into a bogus near-default value that looked filled but wasn't. Each field is now guarded against its own struct's actual unfilled convention (NaN for structs with no setDefault(), -9999. for setDefault()-based structs and the upstream SimpleFlashMatch producer's own sentinel), confirmed individually from source and empirically rather than assumed uniform. --- sbncode/CAFMaker/CAFMaker_module.cc | 95 +++++++++++++++++++---------- 1 file changed, 63 insertions(+), 32 deletions(-) diff --git a/sbncode/CAFMaker/CAFMaker_module.cc b/sbncode/CAFMaker/CAFMaker_module.cc index 6dc8560a9..f05df3ec3 100644 --- a/sbncode/CAFMaker/CAFMaker_module.cc +++ b/sbncode/CAFMaker/CAFMaker_module.cc @@ -548,9 +548,10 @@ void CAFMaker::SBNDShiftCRTReference(StandardRecord &rec, double SBNDFrame) cons if(!std::isnan(pfp.trk.crtsbndtrack.score)) pfp.trk.crtsbndtrack.track.time += SBNDFrame; } - // CRUMBS CRT input features [us] - slc.crumbs_result.crt.tracktime += SBNDFrame / 1000.; - slc.crumbs_result.crt.sptime += SBNDFrame / 1000.; + // CRUMBS CRT input features [us]. SRCRUMBSResult has no setDefault() -- + // "unfilled" is the plain default-constructed NaN, not -9999. + if (!std::isnan(slc.crumbs_result.crt.tracktime)) slc.crumbs_result.crt.tracktime += SBNDFrame / 1000.; + if (!std::isnan(slc.crumbs_result.crt.sptime)) slc.crumbs_result.crt.sptime += SBNDFrame / 1000.; } // TODO: SRSBNDCRTVeto.sp_time (FillReco.cxx) is not shifted here -- watch list. @@ -605,22 +606,41 @@ void CAFMaker::CorrectMCTiming(StandardRecord &rec) const { if (part.endT > -9998.) part.endT += delta_us; } - slc.opt0.time += delta_us; - slc.opt0_sec.time += delta_us; + // Every field below comes from an optional reco association that isn't + // guaranteed to exist for a given slice (OpT0Finder candidate, + // TPCPMTBarycenterMatch, SimpleFlashMatch, CRUMBSResult); an absent one + // leaves its CAF field at that struct's own "unfilled" default. Each + // struct only ever uses ONE convention (confirmed from source + + // empirically), so each guard below checks only that field's actual + // sentinel: SROpT0Finder/SRCRUMBSResult have no setDefault(), just a + // NaN-initializing constructor, so those fields guard on isnan() alone; + // SRTPCPMTBarycenterMatch/SRCorrectedOpFlash/sbn::SimpleFlashMatch (the + // fmatch* upstream producer, copied through even when absent) default + // to -9999. via setDefault()/the producer's own sentinel, so those guard + // on != -9999. alone. Shifting an unfilled default blindly turns an + // honest "not filled" sentinel into a bogus near-default value that + // looks filled but isn't -- same bug class as the correctedOpFlash fix. + if (!std::isnan(slc.opt0.time)) slc.opt0.time += delta_us; // NaN default + if (!std::isnan(slc.opt0_sec.time)) slc.opt0_sec.time += delta_us; // NaN default - slc.barycenterFM.flashTime += delta_us; - slc.barycenterFM.flashFirstHit += delta_us; + if (slc.correctedOpFlash.OpFlashT0 != -9999.) { // -9999. default + slc.correctedOpFlash.OpFlashT0 += delta_us; + slc.correctedOpFlash.OpFlashT0Corrected += delta_us; + } + + if (slc.barycenterFM.flashTime != -9999.) slc.barycenterFM.flashTime += delta_us; // -9999. default + if (slc.barycenterFM.flashFirstHit != -9999.) slc.barycenterFM.flashFirstHit += delta_us; // -9999. default // PMT/XARAPUCA simple flash matches (fmatch=PMT-SimpleFlash, fmatchop=PMT-OpFlash, // fmatchara=XARAPUCA-SimpleFlash, fmatchopara=XARAPUCA-OpFlash) - slc.fmatch.time += delta_us; - slc.fmatchop.time += delta_us; - slc.fmatchara.time += delta_us; - slc.fmatchopara.time += delta_us; + if (slc.fmatch.time != -9999.) slc.fmatch.time += delta_us; // -9999. default + if (slc.fmatchop.time != -9999.) slc.fmatchop.time += delta_us; // -9999. default + if (slc.fmatchara.time != -9999.) slc.fmatchara.time += delta_us; // -9999. default + if (slc.fmatchopara.time != -9999.) slc.fmatchopara.time += delta_us; // -9999. default - slc.crumbs_result.pds.fmtime += delta_us; - slc.crumbs_result.crt.tracktime += delta_us; - slc.crumbs_result.crt.sptime += delta_us; + if (!std::isnan(slc.crumbs_result.pds.fmtime)) slc.crumbs_result.pds.fmtime += delta_us; // NaN default + if (!std::isnan(slc.crumbs_result.crt.tracktime)) slc.crumbs_result.crt.tracktime += delta_us; // NaN default + if (!std::isnan(slc.crumbs_result.crt.sptime)) slc.crumbs_result.crt.sptime += delta_us; // NaN default for (SRPFP &pfp: slc.reco.pfp) { if (!std::isnan(pfp.trk.crtspacepoint.score)) pfp.trk.crtspacepoint.spacepoint.time += off_ns; @@ -639,8 +659,9 @@ void CAFMaker::CorrectMCTiming(StandardRecord &rec) const { // CRTAnalysis analyzer, not persisted as an art product; // CAFMaker would need to call it directly (not done here) // SRSBNDCRTVeto.sp_time - // SRCorrectedOpFlash (slice.correctedOpFlash); see - // https://github.com/SBNSoftware/sbncode/pull/668#pullrequestreview-5117853839 + // SRCorrectedOpFlash.NuToFLight/.NuToFCharge (slice.correctedOpFlash) -- these are + // time-of-flight differences between already-shifted quantities, so a constant + // offset cancels; OpFlashT0/OpFlashT0Corrected are corrected above. // SRSoftwareTrigger.flash_peaktime } @@ -654,24 +675,31 @@ void CAFMaker::SBNDShiftPMTReference(StandardRecord &rec, double SBNDFrame) cons opf.firsttime += SBNDFrame_us; } - //OpT0 match to slice + //OpT0 match to slice. Guards match each field's actual "unfilled" + //convention (NaN via plain constructor, or -9999. via setDefault()/the + //upstream producer's own sentinel) -- see CorrectMCTiming for the detail. for (SRSlice &s: rec.slc) { - s.opt0.time += SBNDFrame_us; - s.opt0_sec.time += SBNDFrame_us; + if (!std::isnan(s.opt0.time)) s.opt0.time += SBNDFrame_us; // NaN default + if (!std::isnan(s.opt0_sec.time)) s.opt0_sec.time += SBNDFrame_us; // NaN default + + if (s.correctedOpFlash.OpFlashT0 != -9999.) { // -9999. default + s.correctedOpFlash.OpFlashT0 += SBNDFrame_us; + s.correctedOpFlash.OpFlashT0Corrected += SBNDFrame_us; + } - s.barycenterFM.flashTime += SBNDFrame_us; - s.barycenterFM.flashFirstHit += SBNDFrame_us; + if (s.barycenterFM.flashTime != -9999.) s.barycenterFM.flashTime += SBNDFrame_us; // -9999. default + if (s.barycenterFM.flashFirstHit != -9999.) s.barycenterFM.flashFirstHit += SBNDFrame_us; // -9999. default - s.fmatch.time += SBNDFrame_us; - s.fmatchop.time += SBNDFrame_us; - s.fmatchara.time += SBNDFrame_us; - s.fmatchopara.time += SBNDFrame_us; + if (s.fmatch.time != -9999.) s.fmatch.time += SBNDFrame_us; // -9999. default + if (s.fmatchop.time != -9999.) s.fmatchop.time += SBNDFrame_us; // -9999. default + if (s.fmatchara.time != -9999.) s.fmatchara.time += SBNDFrame_us; // -9999. default + if (s.fmatchopara.time != -9999.) s.fmatchopara.time += SBNDFrame_us; // -9999. default - s.crumbs_result.pds.fmtime += SBNDFrame_us; + if (!std::isnan(s.crumbs_result.pds.fmtime)) s.crumbs_result.pds.fmtime += SBNDFrame_us; // NaN default } - // TODO: SRCorrectedOpFlash (slice.correctedOpFlash) not yet shifted here; see - // https://github.com/SBNSoftware/sbncode/pull/668#pullrequestreview-5117853839 + // TODO: SRCorrectedOpFlash.NuToFLight/.NuToFCharge not shifted here -- ToF/duration + // quantities, a constant offset cancels (see CorrectMCTiming for the same reasoning). // TODO: SRSoftwareTrigger.flash_peaktime not yet shifted here. } @@ -683,12 +711,15 @@ void CAFMaker::FixPMTReferenceTimes(StandardRecord &rec, double PMT_reference_ti f.firsttime += PMT_reference_time; } - // Fix the flash matches + // Fix the flash matches. Both fields default to -9999. (setDefault()/the + // upstream producer's own sentinel), not NaN -- see CorrectMCTiming for + // the detail. Runs unconditionally for MC and data, so the guard matters + // on both paths. for (SRSlice &s: rec.slc) { - s.fmatch.time += PMT_reference_time; + if (s.fmatch.time != -9999.) s.fmatch.time += PMT_reference_time; - s.barycenterFM.flashTime +=PMT_reference_time; - s.barycenterFM.flashFirstHit +=PMT_reference_time; + if (s.barycenterFM.flashTime != -9999.) s.barycenterFM.flashTime += PMT_reference_time; + if (s.barycenterFM.flashFirstHit != -9999.) s.barycenterFM.flashFirstHit += PMT_reference_time; } // TODO: fix more? From 849bf3a6970a8f7d9a4cb491a2528cc65a27611c Mon Sep 17 00:00:00 2001 From: VCLanNguyen Date: Fri, 25 Sep 2026 12:54:35 -0500 Subject: [PATCH 05/15] Scope the dk2nu vertex-time correction to GENIE neutrino interactions CorrectMCTiming() recovers a dk2nu-specific bug (missing meson decay time in the stored vertex time) and so should only ever touch interactions that came from GENIE. But rec.mc.nu is filled from whatever generator produced MCTruth under the configured producer label, not GENIE specifically -- HNL/dissonant- Higgs productions (MeVPrtlGen) reuse the same "generator" label and push their own MCTruth there too. The correction happened to still skip those in current production configs, but only because MeVPrtlGen never produces the bsim::Dk2Nu product the correction depends on -- incidental, not an explicit guarantee. Sets SRTrueInteraction.generator (declared, previously never assigned) from whether a simb::GTruth was associated with the interaction -- GTruth only ever comes from GENIE -- and gates the correction on it directly. --- sbncode/CAFMaker/CAFMaker_module.cc | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/sbncode/CAFMaker/CAFMaker_module.cc b/sbncode/CAFMaker/CAFMaker_module.cc index f05df3ec3..c3b28d905 100644 --- a/sbncode/CAFMaker/CAFMaker_module.cc +++ b/sbncode/CAFMaker/CAFMaker_module.cc @@ -573,6 +573,12 @@ void CAFMaker::CorrectMCTiming(StandardRecord &rec) const { for (unsigned i = 0; i < rec.mc.nu.size(); i++) { SRTrueInteraction &nu = rec.mc.nu[i]; + // This is a GENIE-dk2nu-specific correction (see the bug writeup above); rec.mc.nu can + // also hold non-GENIE interactions (e.g. HNL decays from MeVPrtlGen, which reuses the + // same "generator" producer label), so an explicit generator check comes first -- the + // NaN check on prod_time only happens to also exclude those today because MeVPrtlGen + // never produces a bsim::Dk2Nu product, which is incidental, not a real guarantee. + if (nu.generator != caf::kGENIE) continue; if (std::isnan(nu.prod_time)) continue; // no dk2nu info for this sample/interaction offset_ns[i] = nu.prod_time + nu.dk2gen * 100. / kSpeedOfLight; @@ -1712,6 +1718,13 @@ void CAFMaker::produce(art::Event& evt) noexcept { FillTrueNeutrino(mctruth, mcflux, dk2nu, gtruth, true_particles, id_to_truehit_map, srtruthbranch.nu.back(), i, fActiveVolumes); + // rec.mc.nu is populated from whatever GenLabel() points to, not GENIE + // specifically -- e.g. MeVPrtlGen (HNL/dissonant-Higgs productions) reuses + // the same "generator" producer label and pushes its own MCTruth here too. + // simb::GTruth is only ever produced alongside a GENIE MCTruth, so whether + // one was found (ok, above) is the definitive per-entry generator tag. + srtruthbranch.nu.back().generator = ok ? caf::kGENIE : caf::kUnknownGenerator; + srtruthbranch.nu.back().genie_evtrec_idx = fGenieEventCounter; // GENIE event record From 3ca4d62f572324927076476d2528e5520a1a252f Mon Sep 17 00:00:00 2001 From: VCLanNguyen Date: Fri, 25 Sep 2026 13:06:07 -0500 Subject: [PATCH 06/15] Stop double-correcting correctedOpFlash reference time in data SBNDShiftPMTReference() was shifting slc.correctedOpFlash.OpFlashT0/ .OpFlashT0Corrected by the SBND frame shift, but for data these fields are already reference-frame corrected upstream: LightPropagationCorrection_module.cc computes them from the real SPECTDC RWM/trigger timestamps in its data-only (!fIsMC) branch. Shifting them again here double-corrected the reference time. Same category as soft_trig.flash_peaktime/sbnd_crt_veto.sp_time, already left alone for this reason. MC is unaffected: for MC that module's !fIsMC branch is skipped, so correctedOpFlash is unshifted at the producer level and CorrectMCTiming()'s own dk2nu-offset correction of it remains necessary. --- sbncode/CAFMaker/CAFMaker_module.cc | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/sbncode/CAFMaker/CAFMaker_module.cc b/sbncode/CAFMaker/CAFMaker_module.cc index c3b28d905..77800a45e 100644 --- a/sbncode/CAFMaker/CAFMaker_module.cc +++ b/sbncode/CAFMaker/CAFMaker_module.cc @@ -688,11 +688,6 @@ void CAFMaker::SBNDShiftPMTReference(StandardRecord &rec, double SBNDFrame) cons if (!std::isnan(s.opt0.time)) s.opt0.time += SBNDFrame_us; // NaN default if (!std::isnan(s.opt0_sec.time)) s.opt0_sec.time += SBNDFrame_us; // NaN default - if (s.correctedOpFlash.OpFlashT0 != -9999.) { // -9999. default - s.correctedOpFlash.OpFlashT0 += SBNDFrame_us; - s.correctedOpFlash.OpFlashT0Corrected += SBNDFrame_us; - } - if (s.barycenterFM.flashTime != -9999.) s.barycenterFM.flashTime += SBNDFrame_us; // -9999. default if (s.barycenterFM.flashFirstHit != -9999.) s.barycenterFM.flashFirstHit += SBNDFrame_us; // -9999. default @@ -706,6 +701,10 @@ void CAFMaker::SBNDShiftPMTReference(StandardRecord &rec, double SBNDFrame) cons // TODO: SRCorrectedOpFlash.NuToFLight/.NuToFCharge not shifted here -- ToF/duration // quantities, a constant offset cancels (see CorrectMCTiming for the same reasoning). + // SRCorrectedOpFlash.OpFlashT0/.OpFlashT0Corrected not shifted here (data path only -- + // CorrectMCTiming still shifts them for MC): LightPropagationCorrection_module.cc computes + // them from the real SPECTDC RWM/trigger timestamps for data (!fIsMC), already reference-frame + // corrected upstream of CAFMaker -- shifting again here would double-correct. // TODO: SRSoftwareTrigger.flash_peaktime not yet shifted here. } From 1f749566ab9aec378c4298c7ad898d13b5588afa Mon Sep 17 00:00:00 2001 From: VCLanNguyen Date: Mon, 28 Sep 2026 17:48:26 -0500 Subject: [PATCH 07/15] Guard CorrectMCTiming's dk2gen offset input against its own unfilled default simb::MCFlux::fdk2gen defaults to -999. (its own Reset()), a different sentinel than the -9999. convention every other guard in this function checks. Without this, an interaction with an unfilled dk2gen would compute a wrong-but-not-NaN offset_ns and silently corrupt every field CorrectMCTiming shifts, the same failure class as the earlier -9999. guard fix. Co-Authored-By: Claude Sonnet 5 --- sbncode/CAFMaker/CAFMaker_module.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/sbncode/CAFMaker/CAFMaker_module.cc b/sbncode/CAFMaker/CAFMaker_module.cc index 77800a45e..a16c4152e 100644 --- a/sbncode/CAFMaker/CAFMaker_module.cc +++ b/sbncode/CAFMaker/CAFMaker_module.cc @@ -580,6 +580,7 @@ void CAFMaker::CorrectMCTiming(StandardRecord &rec) const { // never produces a bsim::Dk2Nu product, which is incidental, not a real guarantee. if (nu.generator != caf::kGENIE) continue; if (std::isnan(nu.prod_time)) continue; // no dk2nu info for this sample/interaction + if (nu.dk2gen <= -998.) continue; // simb::MCFlux::fdk2gen unfilled default is -999. offset_ns[i] = nu.prod_time + nu.dk2gen * 100. / kSpeedOfLight; double const delta_us = offset_ns[i] / 1000.; From bf1b2c92b9173c2d4701468e878bacde9edd8466 Mon Sep 17 00:00:00 2001 From: VCLanNguyen Date: Mon, 28 Sep 2026 17:48:35 -0500 Subject: [PATCH 08/15] Condense and correct comments in the dk2nu/RWM timing-correction functions Trim CorrectMCTiming()/SBNDShiftCRTReference()/SBNDShiftPMTReference()/ FixPMTReferenceTimes() comments from prose paragraphs to short per-field labels, and record that SRSBNDCRTVeto.sp_time, SRSoftwareTrigger.flash_peaktime, and correctedOpFlash.OpFlashT0/.OpFlashT0Corrected (data path) are already reference-frame corrected upstream in their own producer modules (CRTVeto, SoftwareTrigger, LightPropagationCorrection) rather than left as an open watch-list item. Co-Authored-By: Claude Sonnet 5 --- sbncode/CAFMaker/CAFMaker_module.cc | 115 +++++++++------------------- 1 file changed, 35 insertions(+), 80 deletions(-) diff --git a/sbncode/CAFMaker/CAFMaker_module.cc b/sbncode/CAFMaker/CAFMaker_module.cc index a16c4152e..742dc9ba8 100644 --- a/sbncode/CAFMaker/CAFMaker_module.cc +++ b/sbncode/CAFMaker/CAFMaker_module.cc @@ -540,45 +540,34 @@ void CAFMaker::SBNDShiftCRTReference(StandardRecord &rec, double SBNDFrame) cons trk.time += SBNDFrame; //ns } - //TODO: CRT Space Point and Track Match for (SRSlice &slc: rec.slc){ for (SRPFP &pfp: slc.reco.pfp){ + //PFP track-matched CRT if(!std::isnan(pfp.trk.crtspacepoint.score)) pfp.trk.crtspacepoint.spacepoint.time += SBNDFrame; - if(!std::isnan(pfp.trk.crtsbndtrack.score)) pfp.trk.crtsbndtrack.track.time += SBNDFrame; } - // CRUMBS CRT input features [us]. SRCRUMBSResult has no setDefault() -- - // "unfilled" is the plain default-constructed NaN, not -9999. + // CRUMBS CRT input features [us]; unfilled default is NaN, not -9999. if (!std::isnan(slc.crumbs_result.crt.tracktime)) slc.crumbs_result.crt.tracktime += SBNDFrame / 1000.; if (!std::isnan(slc.crumbs_result.crt.sptime)) slc.crumbs_result.crt.sptime += SBNDFrame / 1000.; } - // TODO: SRSBNDCRTVeto.sp_time (FillReco.cxx) is not shifted here -- watch list. + // Note: SRSBNDCRTVeto.sp_time data is already RWM-shifted in CRTVeto module } void CAFMaker::CorrectMCTiming(StandardRecord &rec) const { - // Recovers the dk2nu v01_11_00 SetT(0) bug (fixed in v01_12_00) - // by adding the missing meson decay time + neutrino tof from meson decay to flux window + // Recovers the dk2nu v01_11_00 SetT(0) bug (fixed in v01_12_00) by adding back + // the missing meson decay time + tof from decay to flux window. constexpr double kSpeedOfLight = 29.9792458; // cm/ns - // TODO: Does not correct TPC timing variables yet: - // SRCaloPoint.t, SRHit.peakTime, SRPFP.t0, - // SRBlip.time/.timeTick, - // SRBlipHitClust.time/.startTime/.endTime/.timespan/.timeTick - // Per-neutrino offset, indexed the same as rec.mc.nu / SRSlice.tmatch.index. std::vector offset_ns(rec.mc.nu.size(), std::numeric_limits::signaling_NaN()); for (unsigned i = 0; i < rec.mc.nu.size(); i++) { SRTrueInteraction &nu = rec.mc.nu[i]; - // This is a GENIE-dk2nu-specific correction (see the bug writeup above); rec.mc.nu can - // also hold non-GENIE interactions (e.g. HNL decays from MeVPrtlGen, which reuses the - // same "generator" producer label), so an explicit generator check comes first -- the - // NaN check on prod_time only happens to also exclude those today because MeVPrtlGen - // never produces a bsim::Dk2Nu product, which is incidental, not a real guarantee. - if (nu.generator != caf::kGENIE) continue; + + if (nu.generator != caf::kGENIE) continue; //GENIE-dk2nu-specific correction; if (std::isnan(nu.prod_time)) continue; // no dk2nu info for this sample/interaction if (nu.dk2gen <= -998.) continue; // simb::MCFlux::fdk2gen unfilled default is -999. @@ -594,9 +583,7 @@ void CAFMaker::CorrectMCTiming(StandardRecord &rec) const { } } - // Apply the matched neutrino's own offset to slice-scoped reco objects (and the - // slice's stale truth copy). SRSlice.tmatch.index (set by MatchSlice2Truth, - // purity/charge-based, independent of nu.time) indexes directly into rec.mc.nu. + // Apply each slice's matched neutrino's own offset (SRSlice.tmatch.index) // Unmatched/cosmic slices (tmatch.index < 0) are left untouched. for (SRSlice &slc: rec.slc) { if (slc.tmatch.index < 0 || std::isnan(offset_ns[slc.tmatch.index])) continue; @@ -604,8 +591,7 @@ void CAFMaker::CorrectMCTiming(StandardRecord &rec) const { double const off_ns = offset_ns[slc.tmatch.index]; double const delta_us = off_ns / 1000.; - // slc.truth is a copy of rec.mc.nu[tmatch.index] taken before this function - // runs (FillTrue.cxx:372) -- needs the same correction applied separately. + // slc.truth is a copy of rec.mc.nu[tmatch.index] taken earlier (FillTrue.cxx:372). slc.truth.time += delta_us; for (SRTrueParticle &part: slc.truth.prim) { if (part.genT > -9998.) part.genT += delta_us; @@ -613,63 +599,42 @@ void CAFMaker::CorrectMCTiming(StandardRecord &rec) const { if (part.endT > -9998.) part.endT += delta_us; } - // Every field below comes from an optional reco association that isn't - // guaranteed to exist for a given slice (OpT0Finder candidate, - // TPCPMTBarycenterMatch, SimpleFlashMatch, CRUMBSResult); an absent one - // leaves its CAF field at that struct's own "unfilled" default. Each - // struct only ever uses ONE convention (confirmed from source + - // empirically), so each guard below checks only that field's actual - // sentinel: SROpT0Finder/SRCRUMBSResult have no setDefault(), just a - // NaN-initializing constructor, so those fields guard on isnan() alone; - // SRTPCPMTBarycenterMatch/SRCorrectedOpFlash/sbn::SimpleFlashMatch (the - // fmatch* upstream producer, copied through even when absent) default - // to -9999. via setDefault()/the producer's own sentinel, so those guard - // on != -9999. alone. Shifting an unfilled default blindly turns an - // honest "not filled" sentinel into a bogus near-default value that - // looks filled but isn't -- same bug class as the correctedOpFlash fix. + // Opt0 FM if (!std::isnan(slc.opt0.time)) slc.opt0.time += delta_us; // NaN default if (!std::isnan(slc.opt0_sec.time)) slc.opt0_sec.time += delta_us; // NaN default + // BFM + if (slc.barycenterFM.flashTime != -9999.) slc.barycenterFM.flashTime += delta_us; // -9999. default + if (slc.barycenterFM.flashFirstHit != -9999.) slc.barycenterFM.flashFirstHit += delta_us; // -9999. default + + // BFM + Light Propagation Module if (slc.correctedOpFlash.OpFlashT0 != -9999.) { // -9999. default slc.correctedOpFlash.OpFlashT0 += delta_us; slc.correctedOpFlash.OpFlashT0Corrected += delta_us; } - if (slc.barycenterFM.flashTime != -9999.) slc.barycenterFM.flashTime += delta_us; // -9999. default - if (slc.barycenterFM.flashFirstHit != -9999.) slc.barycenterFM.flashFirstHit += delta_us; // -9999. default - - // PMT/XARAPUCA simple flash matches (fmatch=PMT-SimpleFlash, fmatchop=PMT-OpFlash, - // fmatchara=XARAPUCA-SimpleFlash, fmatchopara=XARAPUCA-OpFlash) + // Simple FM: PMT & XARAPUCA if (slc.fmatch.time != -9999.) slc.fmatch.time += delta_us; // -9999. default if (slc.fmatchop.time != -9999.) slc.fmatchop.time += delta_us; // -9999. default if (slc.fmatchara.time != -9999.) slc.fmatchara.time += delta_us; // -9999. default if (slc.fmatchopara.time != -9999.) slc.fmatchopara.time += delta_us; // -9999. default - if (!std::isnan(slc.crumbs_result.pds.fmtime)) slc.crumbs_result.pds.fmtime += delta_us; // NaN default - if (!std::isnan(slc.crumbs_result.crt.tracktime)) slc.crumbs_result.crt.tracktime += delta_us; // NaN default - if (!std::isnan(slc.crumbs_result.crt.sptime)) slc.crumbs_result.crt.sptime += delta_us; // NaN default - + //PFP track-matched CRT for (SRPFP &pfp: slc.reco.pfp) { if (!std::isnan(pfp.trk.crtspacepoint.score)) pfp.trk.crtspacepoint.spacepoint.time += off_ns; if (!std::isnan(pfp.trk.crtsbndtrack.score)) pfp.trk.crtsbndtrack.track.time += off_ns; } + + // CRUMBS BDT + if (!std::isnan(slc.crumbs_result.pds.fmtime)) slc.crumbs_result.pds.fmtime += delta_us; // NaN default + if (!std::isnan(slc.crumbs_result.crt.tracktime)) slc.crumbs_result.crt.tracktime += delta_us; // NaN default + if (!std::isnan(slc.crumbs_result.crt.sptime)) slc.crumbs_result.crt.sptime += delta_us; // NaN default } - // TODO: watch list -- no truth-matching path currently exists to assign a - // per-object offset to these EVENT-GLOBAL (not slice-scoped) collections: - // rec.opflashes -- would need a PhotonBackTracker-based match; none - // exists in SBND today (OpDetBacktrackerRecords are - // dropped from reco output; PhotonBackTracker unused) - // rec.crt_spacepoints, rec.sbnd_crt_tracks - // -- CRTBackTrackerAlg exists (sbndcode CRT/CRTBackTracker/ - // CRTBackTrackerAlg.{h,cc}) but is only invoked from the - // CRTAnalysis analyzer, not persisted as an art product; - // CAFMaker would need to call it directly (not done here) - // SRSBNDCRTVeto.sp_time - // SRCorrectedOpFlash.NuToFLight/.NuToFCharge (slice.correctedOpFlash) -- these are - // time-of-flight differences between already-shifted quantities, so a constant - // offset cancels; OpFlashT0/OpFlashT0Corrected are corrected above. - // SRSoftwareTrigger.flash_peaktime + // TODO: Non-correction list + // 1. No slice-truth-matching path exists: rec.opflashes, rec.crt_spacepoints, rec.sbnd_crt_tracks + // 2. TPC timing variables: SRCaloPoint.t, SRHit.peakTime, SRPFP.t0, SRBlip.time/.timeTick, SRBlipHitClust.time/.startTime/.endTime/.timespan/.timeTick + // 3. Has their own timing shift in local modules: SRSBNDCRTVeto.sp_time, SRSoftwareTrigger.flash_peaktime } void CAFMaker::SBNDShiftPMTReference(StandardRecord &rec, double SBNDFrame) const { @@ -682,9 +647,7 @@ void CAFMaker::SBNDShiftPMTReference(StandardRecord &rec, double SBNDFrame) cons opf.firsttime += SBNDFrame_us; } - //OpT0 match to slice. Guards match each field's actual "unfilled" - //convention (NaN via plain constructor, or -9999. via setDefault()/the - //upstream producer's own sentinel) -- see CorrectMCTiming for the detail. + // OpT0 match to slice; guards match each field's own unfilled convention (see CorrectMCTiming) for (SRSlice &s: rec.slc) { if (!std::isnan(s.opt0.time)) s.opt0.time += SBNDFrame_us; // NaN default if (!std::isnan(s.opt0_sec.time)) s.opt0_sec.time += SBNDFrame_us; // NaN default @@ -700,13 +663,9 @@ void CAFMaker::SBNDShiftPMTReference(StandardRecord &rec, double SBNDFrame) cons if (!std::isnan(s.crumbs_result.pds.fmtime)) s.crumbs_result.pds.fmtime += SBNDFrame_us; // NaN default } - // TODO: SRCorrectedOpFlash.NuToFLight/.NuToFCharge not shifted here -- ToF/duration - // quantities, a constant offset cancels (see CorrectMCTiming for the same reasoning). - // SRCorrectedOpFlash.OpFlashT0/.OpFlashT0Corrected not shifted here (data path only -- - // CorrectMCTiming still shifts them for MC): LightPropagationCorrection_module.cc computes - // them from the real SPECTDC RWM/trigger timestamps for data (!fIsMC), already reference-frame - // corrected upstream of CAFMaker -- shifting again here would double-correct. - // TODO: SRSoftwareTrigger.flash_peaktime not yet shifted here. + // Note: + // 1. SRSoftwareTrigger.flash_peaktime data: already RWM-shifted in SoftwareTrigger module + // 2. correctedOpFlash.OpFlashT0/.OpFlashT0Corrected: already RWM-shifted in LightPropagation module } void CAFMaker::FixPMTReferenceTimes(StandardRecord &rec, double PMT_reference_time) { @@ -717,10 +676,8 @@ void CAFMaker::FixPMTReferenceTimes(StandardRecord &rec, double PMT_reference_ti f.firsttime += PMT_reference_time; } - // Fix the flash matches. Both fields default to -9999. (setDefault()/the - // upstream producer's own sentinel), not NaN -- see CorrectMCTiming for - // the detail. Runs unconditionally for MC and data, so the guard matters - // on both paths. + // Fix the flash matches. Runs unconditionally for MC and data, so the + // -9999. unfilled-default guard matters on both paths (see CorrectMCTiming). for (SRSlice &s: rec.slc) { if (s.fmatch.time != -9999.) s.fmatch.time += PMT_reference_time; @@ -1718,11 +1675,9 @@ void CAFMaker::produce(art::Event& evt) noexcept { FillTrueNeutrino(mctruth, mcflux, dk2nu, gtruth, true_particles, id_to_truehit_map, srtruthbranch.nu.back(), i, fActiveVolumes); - // rec.mc.nu is populated from whatever GenLabel() points to, not GENIE - // specifically -- e.g. MeVPrtlGen (HNL/dissonant-Higgs productions) reuses - // the same "generator" producer label and pushes its own MCTruth here too. - // simb::GTruth is only ever produced alongside a GENIE MCTruth, so whether - // one was found (ok, above) is the definitive per-entry generator tag. + // rec.mc.nu isn't GENIE-only -- MeVPrtlGen (HNL/Higgs) reuses the same + // "generator" label. GTruth only exists alongside a GENIE MCTruth, so + // whether one was found (ok) is the definitive per-entry generator tag. srtruthbranch.nu.back().generator = ok ? caf::kGENIE : caf::kUnknownGenerator; srtruthbranch.nu.back().genie_evtrec_idx = fGenieEventCounter; From 9752c09b325611d6496ece378b14488f0b97fd05 Mon Sep 17 00:00:00 2001 From: VCLanNguyen Date: Mon, 28 Sep 2026 18:07:53 -0500 Subject: [PATCH 09/15] Drop redundant flash-match guard comment in FixPMTReferenceTimes Co-Authored-By: Claude Sonnet 5 --- sbncode/CAFMaker/CAFMaker_module.cc | 2 -- 1 file changed, 2 deletions(-) diff --git a/sbncode/CAFMaker/CAFMaker_module.cc b/sbncode/CAFMaker/CAFMaker_module.cc index 742dc9ba8..16d9d75ce 100644 --- a/sbncode/CAFMaker/CAFMaker_module.cc +++ b/sbncode/CAFMaker/CAFMaker_module.cc @@ -676,8 +676,6 @@ void CAFMaker::FixPMTReferenceTimes(StandardRecord &rec, double PMT_reference_ti f.firsttime += PMT_reference_time; } - // Fix the flash matches. Runs unconditionally for MC and data, so the - // -9999. unfilled-default guard matters on both paths (see CorrectMCTiming). for (SRSlice &s: rec.slc) { if (s.fmatch.time != -9999.) s.fmatch.time += PMT_reference_time; From 401aa8fafddcf2c0961cc3deb7b323b7fbc28bf8 Mon Sep 17 00:00:00 2001 From: VCLanNguyen Date: Mon, 28 Sep 2026 18:08:01 -0500 Subject: [PATCH 10/15] Distinguish kMeVPrtl from kUnknownGenerator when tagging rec.mc.nu.generator Previously any MCTruth without an associated GTruth (i.e. non-GENIE) was tagged kUnknownGenerator, even though MeVPrtlGen_module.cc pushes a matching MeVPrtlTruth into the same event at the same index as its MCTruth (confirmed in its produce(): the two collections are always pushed back-to-back in the same loop iteration). Use that index alignment -- the same assumption this function already relies on for mcfluxes.at(i)/dk2nus.at(i) -- to tag those entries kMeVPrtl instead. caf::kMeVPrtl already existed in SREnums.h but nothing was setting it. CorrectMCTiming()'s existing `generator != caf::kGENIE` guard needs no change: kMeVPrtl still isn't kGENIE, so those slices are still skipped. Co-Authored-By: Claude Sonnet 5 --- sbncode/CAFMaker/CAFMaker_module.cc | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/sbncode/CAFMaker/CAFMaker_module.cc b/sbncode/CAFMaker/CAFMaker_module.cc index 16d9d75ce..fa30199c7 100644 --- a/sbncode/CAFMaker/CAFMaker_module.cc +++ b/sbncode/CAFMaker/CAFMaker_module.cc @@ -1673,10 +1673,13 @@ void CAFMaker::produce(art::Event& evt) noexcept { FillTrueNeutrino(mctruth, mcflux, dk2nu, gtruth, true_particles, id_to_truehit_map, srtruthbranch.nu.back(), i, fActiveVolumes); - // rec.mc.nu isn't GENIE-only -- MeVPrtlGen (HNL/Higgs) reuses the same - // "generator" label. GTruth only exists alongside a GENIE MCTruth, so - // whether one was found (ok) is the definitive per-entry generator tag. - srtruthbranch.nu.back().generator = ok ? caf::kGENIE : caf::kUnknownGenerator; + if (ok) { + srtruthbranch.nu.back().generator = caf::kGENIE; + } else if (i < mevprtl_truths.size()) { + srtruthbranch.nu.back().generator = caf::kMeVPrtl; + } else { + srtruthbranch.nu.back().generator = caf::kUnknownGenerator; + } srtruthbranch.nu.back().genie_evtrec_idx = fGenieEventCounter; From 5382fef12959e2e25d23e46e98554fb4d22d37e6 Mon Sep 17 00:00:00 2001 From: VCLanNguyen Date: Mon, 28 Sep 2026 19:17:01 -0500 Subject: [PATCH 11/15] Revert unfilled-default guard on FixPMTReferenceTimes This function runs unconditionally for both MC and data with no detector gate (called for ICARUS too), so guarding it is out of scope for this SBND-only dk2nu/gate-shift project -- back to unconditionally shifting fmatch.time/barycenterFM.flashTime/.flashFirstHit, matching pre-guard-pass behavior byte-for-byte (comment included), so ICARUS CAF output is unaffected. --- sbncode/CAFMaker/CAFMaker_module.cc | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/sbncode/CAFMaker/CAFMaker_module.cc b/sbncode/CAFMaker/CAFMaker_module.cc index fa30199c7..fdf9dfb36 100644 --- a/sbncode/CAFMaker/CAFMaker_module.cc +++ b/sbncode/CAFMaker/CAFMaker_module.cc @@ -676,11 +676,12 @@ void CAFMaker::FixPMTReferenceTimes(StandardRecord &rec, double PMT_reference_ti f.firsttime += PMT_reference_time; } + // Fix the flash matches for (SRSlice &s: rec.slc) { - if (s.fmatch.time != -9999.) s.fmatch.time += PMT_reference_time; + s.fmatch.time += PMT_reference_time; - if (s.barycenterFM.flashTime != -9999.) s.barycenterFM.flashTime += PMT_reference_time; - if (s.barycenterFM.flashFirstHit != -9999.) s.barycenterFM.flashFirstHit += PMT_reference_time; + s.barycenterFM.flashTime +=PMT_reference_time; + s.barycenterFM.flashFirstHit +=PMT_reference_time; } // TODO: fix more? From a5e44570850367d41ae04dba4d6cebd36a0dbbdd Mon Sep 17 00:00:00 2001 From: VCLanNguyen Date: Tue, 29 Sep 2026 16:13:23 -0500 Subject: [PATCH 12/15] Fill MeVPrtl production-time from meson_dpos_beamcoord Companion to the sbnanaobj SRMeVPrtl::prod_time field: sources it from MeVPrtlTruth::meson_dpos_beamcoord.T(), the clean pre-beam-offset meson decay time (already threaded from the BNB flux ntuple's ini_t[0]). Co-Authored-By: Claude Sonnet 5 --- sbncode/CAFMaker/FillTrue.cxx | 1 + 1 file changed, 1 insertion(+) diff --git a/sbncode/CAFMaker/FillTrue.cxx b/sbncode/CAFMaker/FillTrue.cxx index 9d291b5fe..4f8a2e730 100644 --- a/sbncode/CAFMaker/FillTrue.cxx +++ b/sbncode/CAFMaker/FillTrue.cxx @@ -387,6 +387,7 @@ namespace caf { srtruth.position.y = truth.decay_pos.Y(); srtruth.position.z = truth.decay_pos.Z(); srtruth.time = truth.decay_pos.T(); + srtruth.prod_time = truth.meson_dpos_beamcoord.T(); srtruth.momentum.x = truth.mevprtl_mom.X(); srtruth.momentum.y = truth.mevprtl_mom.Y(); From 0831e92226a19d4c0f26e739b5288e45875d8be9 Mon Sep 17 00:00:00 2001 From: VCLanNguyen Date: Tue, 29 Sep 2026 19:43:08 -0500 Subject: [PATCH 13/15] Comment MeVPrtl prod_time fill to match dev tree wording Co-Authored-By: Claude Sonnet 5 --- sbncode/CAFMaker/FillTrue.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sbncode/CAFMaker/FillTrue.cxx b/sbncode/CAFMaker/FillTrue.cxx index 4f8a2e730..8be94a3f6 100644 --- a/sbncode/CAFMaker/FillTrue.cxx +++ b/sbncode/CAFMaker/FillTrue.cxx @@ -387,7 +387,7 @@ namespace caf { srtruth.position.y = truth.decay_pos.Y(); srtruth.position.z = truth.decay_pos.Z(); srtruth.time = truth.decay_pos.T(); - srtruth.prod_time = truth.meson_dpos_beamcoord.T(); + srtruth.prod_time = truth.meson_dpos_beamcoord.T(); //Meson time from flux file. srtruth.momentum.x = truth.mevprtl_mom.X(); srtruth.momentum.y = truth.mevprtl_mom.Y(); From 31fad5ae45615e76664cb50332e2099c813cb627 Mon Sep 17 00:00:00 2001 From: VCLanNguyen Date: Tue, 29 Sep 2026 19:47:16 -0500 Subject: [PATCH 14/15] Add debug print to CorrectMCTiming to confirm it runs Co-Authored-By: Claude Sonnet 5 --- sbncode/CAFMaker/CAFMaker_module.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sbncode/CAFMaker/CAFMaker_module.cc b/sbncode/CAFMaker/CAFMaker_module.cc index fdf9dfb36..a74bcf4ff 100644 --- a/sbncode/CAFMaker/CAFMaker_module.cc +++ b/sbncode/CAFMaker/CAFMaker_module.cc @@ -557,6 +557,8 @@ void CAFMaker::SBNDShiftCRTReference(StandardRecord &rec, double SBNDFrame) cons void CAFMaker::CorrectMCTiming(StandardRecord &rec) const { + std::cout << "CAFMaker::CorrectMCTiming: running" << std::endl; + // Recovers the dk2nu v01_11_00 SetT(0) bug (fixed in v01_12_00) by adding back // the missing meson decay time + tof from decay to flux window. constexpr double kSpeedOfLight = 29.9792458; // cm/ns From c6c16ea5b00e8c30399f277a3838dd571eec903a Mon Sep 17 00:00:00 2001 From: VCLanNguyen Date: Wed, 30 Sep 2026 17:08:24 -0500 Subject: [PATCH 15/15] Add debug print for fSubRunPOT/frameApplyAtCaf gate --- sbncode/CAFMaker/CAFMaker_module.cc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sbncode/CAFMaker/CAFMaker_module.cc b/sbncode/CAFMaker/CAFMaker_module.cc index a74bcf4ff..03afbd8f2 100644 --- a/sbncode/CAFMaker/CAFMaker_module.cc +++ b/sbncode/CAFMaker/CAFMaker_module.cc @@ -2835,6 +2835,10 @@ void CAFMaker::produce(art::Event& evt) noexcept { if (isRealData && (fDet == kSBND) && fSubRunPOT > 0) { + std::cout << "CAFMaker: run " << evt.run() << " subRun " << evt.subRun() + << " fSubRunPOT=" << fSubRunPOT + << " frameApplyAtCaf=" << rec.sbnd_frames.frameApplyAtCaf << std::endl; + // Fill trigger info FillTriggerSBND(srsbndtiminginfo, srtrigger);