From c4b5470fe0d74033433139ffe059cac01f9e5599 Mon Sep 17 00:00:00 2001 From: Matteo Vicenzi Date: Thu, 18 Jun 2026 14:57:22 -0700 Subject: [PATCH 01/10] include initial tail suppression --- .../PMT/Algorithms/PMTsimulationAlg.cxx | 29 +++++++ icaruscode/PMT/Algorithms/PMTsimulationAlg.h | 80 ++++++++++++++++++- .../PMT/Algorithms/pmtsimulation_icarus.fcl | 21 +++++ 3 files changed, 128 insertions(+), 2 deletions(-) diff --git a/icaruscode/PMT/Algorithms/PMTsimulationAlg.cxx b/icaruscode/PMT/Algorithms/PMTsimulationAlg.cxx index e30bf2cc7..96c122510 100644 --- a/icaruscode/PMT/Algorithms/PMTsimulationAlg.cxx +++ b/icaruscode/PMT/Algorithms/PMTsimulationAlg.cxx @@ -429,6 +429,7 @@ auto icarus::opdet::PMTsimulationAlg::CreateFullWaveform // std::cout << "\tadded pes... " << channel << " " << diff.count() << std::endl; // start=std::chrono::high_resolution_clock::now(); + ApplyTailSuppression(waveform); AddPedestal(channel, waveformStartTS, waveform); if(fParams.darkNoiseRate > 0.0_Hz) AddDarkNoise(waveform, channel); @@ -867,6 +868,27 @@ void icarus::opdet::PMTsimulationAlg::ApplySaturation( } // icarus::opdet::PMTsimulationAlg::ApplySaturation(range) +// ----------------------------------------------------------------------------- +void icarus::opdet::PMTsimulationAlg::ApplyTailSuppression(Waveform_t& waveform) const +{ + if (!fParams.tailSuppression.apply) return; + + // tau [ns] * fSampling [MHz] / 1000 = tau_ticks + double const tau_ticks = fParams.tailSuppression.tau * fSampling.value() / 1000.0; + double const beta = 1.0 / tau_ticks; + double const epsilon = fParams.tailSuppression.epsilon; + + double Q = 0.0; + for (auto& sample : waveform) { + double const s = fParams.pulsePolarity * sample.value(); // original sample + Q = (1.0 - beta) * Q + beta * std::max(0.0, s); // charge accumulation + // apply correction, clamp to avoid negative values + sample = ADCcount{ fParams.pulsePolarity * std::max(0.0, s - epsilon * Q) }; + } + +} // icarus::opdet::PMTsimulationAlg::ApplyTailSuppression() + + // ----------------------------------------------------------------------------- /// Forces `waveform` ADC within the `min` to `max` range (`max` included). void icarus::opdet::PMTsimulationAlg::ClipWaveform @@ -1012,6 +1034,13 @@ icarus::opdet::PMTsimulationAlgMaker::PMTsimulationAlgMaker fBaseConfig.beamGateTriggerNReps = config.BeamGateTriggerNReps(); fBaseConfig.discrimAlgo = config.getDiscriminationAlgo(); + // + // tail suppression + // + fBaseConfig.tailSuppression.apply = config.TailSuppression().Apply(); + fBaseConfig.tailSuppression.epsilon = config.TailSuppression().Epsilon(); + fBaseConfig.tailSuppression.tau = config.TailSuppression().Tau(); + // // parameter checks // diff --git a/icaruscode/PMT/Algorithms/PMTsimulationAlg.h b/icaruscode/PMT/Algorithms/PMTsimulationAlg.h index 71b66c72c..e0b1c01dd 100644 --- a/icaruscode/PMT/Algorithms/PMTsimulationAlg.h +++ b/icaruscode/PMT/Algorithms/PMTsimulationAlg.h @@ -166,6 +166,7 @@ class icarus::opdet::OpDetWaveformMakerClass { * * physical photons * * dark noise * * electronics noise + * * tail suppression * * The algorithm processes an optical channel at a time, independently * and uncorrelated from the other channels. @@ -320,6 +321,28 @@ class icarus::opdet::OpDetWaveformMakerClass { * Noise can be disabled by using the `PMTnoNoiseGeneratorTool`. * * + * Tail suppression + * ----------------- + * + * Data waveforms exhibit a baseline droop in the tail of large pulses: + * after a bright flash, the waveform sits systematically below what + * predicted by linearly scaling the SPE template. This effect is consistent with + * dynode space-charge saturation proportional to the recent integrated charge. + * + * When enabled via `TailSuppression.Apply`, a low-pass filter is applied + * to the signal waveform (pre-pedestal) to accumulate the integrated charge. + * The filter is implemented as a recursive average: + * @f[ + * Q(n) = (1 - \beta)\,Q(n-1) + \beta\,\max(0,\,s(n)) + * @f] + * where @\beta` is the inverse time constant of the filter (`TailSuppression.Tau`). + * The charge state is initialized to zero at the start of each + * channel's full waveform and does not persist across channels. + * + * Based on the accumulated charge, a correction is applied to the signal + * through a tunable strenght parameter `TailSuppression.Epsilon`. + * The parameters of the corrections are the same across all channels. + * * Configuration * ============== * @@ -347,6 +370,13 @@ class icarus::opdet::OpDetWaveformMakerClass { * @f$ \mu_{i} \propto (\Delta V_{i})^{k} @f$ (with @f$ \mu_{i} @f$ the * gain for stage @f$ i @f$, @f$ \Delta V_{i} @f$ the drop of potential * of that stage and @f$ k @f$ the parameter set by `dynodeK`. + * * `TailSuppression` (table, optional): parameters for the tail suppression + * correction (see "Tail suppression" section above). Sub-parameters: + * * `Apply` (default: `false`): enable the correction. + * * `Epsilon` (default: `0.0`): correction strength @f$ \varepsilon @f$ + * (dimensionless); typical values 0.25 (Run 1/2) or 0.30 (Run 3/4). + * * `Tau` (default: `250.0`): charge-state decay time constant [ns]. + * * * `DiscrimAlgo` (choice, default: `"CrossingThreshold"`): selects one of the * hard-coded discrimination algorithms used for zero suppression. * The suppression algorithm identifies some interesting time points and @@ -505,6 +535,12 @@ class icarus::opdet::PMTsimulationAlg { }; // struct PMTspecs_t + struct TailSuppressionParams_t { + bool apply = false; + double epsilon = 0.0; ///< correction strength [dimensionless] + double tau = 250.0; ///< charge-state decay time constant [ns] + }; + /// @{ /// @name High level configuration parameters. @@ -531,6 +567,7 @@ class icarus::opdet::PMTsimulationAlg { hertz darkNoiseRate; float saturation; //equivalent to the number of p.e. that saturates the electronic signal PMTspecs_t PMTspecs; ///< PMT specifications. + TailSuppressionParams_t tailSuppression; ///< Causal subtractive droop correction parameters. bool doGainFluctuations; ///< Whether to simulate gain fluctuations. bool useGainCalibDB; ///< Whether to use per-channel DB gain for fluctuations. bool doTimingDelays; ///< Whether to simulate timing delays. @@ -984,7 +1021,10 @@ class icarus::opdet::PMTsimulationAlg { /// Applies the configured photoelectron saturation on the `waveform`. void ApplySaturation(Waveform_t& waveform, ADCcount baseline) const; - + + /// Applies the causal subtractive tail suppression correction to the signal waveform. + void ApplyTailSuppression(Waveform_t& waveform) const; + /// Forces `waveform` ADC within the `min` to `max` range (`max` included). static void ClipWaveform(Waveform_t& waveform, ADCcount min, ADCcount max); @@ -1034,7 +1074,29 @@ class icarus::opdet::PMTsimulationAlgMaker { }; // struct PMTspecConfig - + struct TailSuppressionConfig { + using Name = fhicl::Name; + using Comment = fhicl::Comment; + + fhicl::Atom Apply { + Name("Apply"), + Comment("Enable causal subtractive tail suppression correction"), + false + }; + fhicl::Atom Epsilon { + Name("Epsilon"), + Comment("Correction strength [dimensionless]"), + 0.0 + }; + fhicl::Atom Tau { + Name("Tau"), + Comment("Charge-state decay time constant [ns]"), + 250.0 + }; + + }; // struct TailSuppressionConfig + + /// Main algorithm FHiCL configuration. struct Config { using Name = fhicl::Name; @@ -1112,6 +1174,14 @@ class icarus::opdet::PMTsimulationAlgMaker { 1U }; + // + // tail suppression + // + fhicl::Table TailSuppression { + Name("TailSuppression"), + Comment("PMT anode baseline-droop correction (causal subtractive model)") + }; + // // dark noise // @@ -1336,6 +1406,12 @@ void icarus::opdet::PMTsimulationAlg::printConfiguration << '\n'; wsp.dump(std::forward(out), indent + " "); + out << '\n' << indent << "Tail suppression: " + << std::boolalpha << fParams.tailSuppression.apply; + if (fParams.tailSuppression.apply) { + out << " (epsilon=" << fParams.tailSuppression.epsilon + << ", tau=" << fParams.tailSuppression.tau << " ns)"; + } out << '\n' << indent << "Track used photons: " << std::boolalpha << fParams.trackSelectedPhotons << '\n'; diff --git a/icaruscode/PMT/Algorithms/pmtsimulation_icarus.fcl b/icaruscode/PMT/Algorithms/pmtsimulation_icarus.fcl index dfd6948bc..fdc3298a9 100644 --- a/icaruscode/PMT/Algorithms/pmtsimulation_icarus.fcl +++ b/icaruscode/PMT/Algorithms/pmtsimulation_icarus.fcl @@ -44,6 +44,8 @@ # 20251111 (mvicenzi@bnl.gov) # introduced configuration for new Run1/2 SPR (SPRisolHitRun9271) # introduced configuration for new Run3/4 SPR (SPRisolHitRun12715) +# 20260618 (mvicenzi@bnl.gov) +# introduced tail suppression tune #include "opticalproperties_icarus.fcl" @@ -402,6 +404,13 @@ icarus_pmtsimulationalg_202202_noise: { Gain: @local::icarus_settings_opdet.nominalPMTgain # total typical PMT gain } # PMTspecs + # Subtractive tail suppression correction + TailSuppression: { + Apply: false + Epsilon: 0.0 + Tau: 250.0 # [ns] + } + } # icarus_pmtsimulationalg_202202_noise @@ -469,6 +478,12 @@ icarus_pmtsimulationalg_run2_2025: { Gain: @local::icarus_settings_opdet_run2.nominalPMTgain # total PMT gain } + TailSuppression: { + Apply: true + Epsilon: 0.25 # fitted on Run1/2 data (run 9384) + Tau: 250.0 # [ns] + } + } # icarus_pmtsimulationalg_run2_2025 @@ -504,6 +519,12 @@ icarus_pmtsimulationalg_run4_2025: { Gain: @local::icarus_settings_opdet_run4.nominalPMTgain # total PMT gain } + TailSuppression: { + Apply: true + Epsilon: 0.30 # fitted on Run3/4 data (run 11816) + Tau: 250.0 # [ns] + } + } # icarus_pmtsimulationalg_run4_2025 # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From b838d6f647b2aca25885f5f403b637bb3feb5b4d Mon Sep 17 00:00:00 2001 From: Matteo Vicenzi Date: Thu, 18 Jun 2026 17:00:48 -0700 Subject: [PATCH 02/10] fix casting --- icaruscode/PMT/Algorithms/PMTsimulationAlg.cxx | 17 ++++++++--------- icaruscode/PMT/Algorithms/PMTsimulationAlg.h | 14 +++++++------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/icaruscode/PMT/Algorithms/PMTsimulationAlg.cxx b/icaruscode/PMT/Algorithms/PMTsimulationAlg.cxx index 96c122510..0cc3dde33 100644 --- a/icaruscode/PMT/Algorithms/PMTsimulationAlg.cxx +++ b/icaruscode/PMT/Algorithms/PMTsimulationAlg.cxx @@ -873,17 +873,16 @@ void icarus::opdet::PMTsimulationAlg::ApplyTailSuppression(Waveform_t& waveform) { if (!fParams.tailSuppression.apply) return; - // tau [ns] * fSampling [MHz] / 1000 = tau_ticks - double const tau_ticks = fParams.tailSuppression.tau * fSampling.value() / 1000.0; - double const beta = 1.0 / tau_ticks; - double const epsilon = fParams.tailSuppression.epsilon; + // tau [ns] * fSampling [MHz] / 1000.0f = tau_ticks + float const beta = 1.0f / (fParams.tailSuppression.tau * fSampling.value() / 1000.0f); + float const epsilon = fParams.tailSuppression.epsilon; + int const pol = fParams.pulsePolarity; - double Q = 0.0; + float Q = 0.0f; for (auto& sample : waveform) { - double const s = fParams.pulsePolarity * sample.value(); // original sample - Q = (1.0 - beta) * Q + beta * std::max(0.0, s); // charge accumulation - // apply correction, clamp to avoid negative values - sample = ADCcount{ fParams.pulsePolarity * std::max(0.0, s - epsilon * Q) }; + float const s = pol * sample.value(); + Q = (1.0f - beta) * Q + beta * std::max(0.0f, s); + sample = ADCcount{ pol * std::max(0.0f, s - epsilon * Q) }; } } // icarus::opdet::PMTsimulationAlg::ApplyTailSuppression() diff --git a/icaruscode/PMT/Algorithms/PMTsimulationAlg.h b/icaruscode/PMT/Algorithms/PMTsimulationAlg.h index e0b1c01dd..ded35c3e2 100644 --- a/icaruscode/PMT/Algorithms/PMTsimulationAlg.h +++ b/icaruscode/PMT/Algorithms/PMTsimulationAlg.h @@ -536,9 +536,9 @@ class icarus::opdet::PMTsimulationAlg { }; // struct PMTspecs_t struct TailSuppressionParams_t { - bool apply = false; - double epsilon = 0.0; ///< correction strength [dimensionless] - double tau = 250.0; ///< charge-state decay time constant [ns] + bool apply = false; + float epsilon = 0.0f; ///< correction strength [dimensionless] + float tau = 250.0f; ///< charge-state decay time constant [ns] }; /// @{ @@ -1083,15 +1083,15 @@ class icarus::opdet::PMTsimulationAlgMaker { Comment("Enable causal subtractive tail suppression correction"), false }; - fhicl::Atom Epsilon { + fhicl::Atom Epsilon { Name("Epsilon"), Comment("Correction strength [dimensionless]"), - 0.0 + 0.0f }; - fhicl::Atom Tau { + fhicl::Atom Tau { Name("Tau"), Comment("Charge-state decay time constant [ns]"), - 250.0 + 250.0f }; }; // struct TailSuppressionConfig From b0427f86875d6deb624877054393d58d7e077d44 Mon Sep 17 00:00:00 2001 From: Matteo Vicenzi Date: Sun, 21 Jun 2026 08:57:32 -0700 Subject: [PATCH 03/10] move printout --- icaruscode/PMT/Algorithms/PMTsimulationAlg.h | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/icaruscode/PMT/Algorithms/PMTsimulationAlg.h b/icaruscode/PMT/Algorithms/PMTsimulationAlg.h index ded35c3e2..05537e74d 100644 --- a/icaruscode/PMT/Algorithms/PMTsimulationAlg.h +++ b/icaruscode/PMT/Algorithms/PMTsimulationAlg.h @@ -1392,6 +1392,13 @@ void icarus::opdet::PMTsimulationAlg::printConfiguration << '\n' << indent << "Bias ratio: " << fBiasConstant ; + out << '\n' << indent << "Tail suppression: " + << std::boolalpha << fParams.tailSuppression.apply; + if (fParams.tailSuppression.apply) { + out << " (epsilon=" << fParams.tailSuppression.epsilon + << ", tau=" << fParams.tailSuppression.tau << " ns)"; + } + out << '\n' << indent << "Pedestal: " << fPedestalGen->toString(indent + " ", ""); if (fParams.createBeamGateTriggers) { @@ -1405,13 +1412,6 @@ void icarus::opdet::PMTsimulationAlg::printConfiguration out << '\n' << indent << "Template photoelectron waveform settings:" << '\n'; wsp.dump(std::forward(out), indent + " "); - - out << '\n' << indent << "Tail suppression: " - << std::boolalpha << fParams.tailSuppression.apply; - if (fParams.tailSuppression.apply) { - out << " (epsilon=" << fParams.tailSuppression.epsilon - << ", tau=" << fParams.tailSuppression.tau << " ns)"; - } out << '\n' << indent << "Track used photons: " << std::boolalpha << fParams.trackSelectedPhotons << '\n'; From 7a1feb69198582bfd2c777c6a3a1c50c356d8c85 Mon Sep 17 00:00:00 2001 From: Matteo Vicenzi Date: Mon, 22 Jun 2026 22:54:50 -0700 Subject: [PATCH 04/10] fix time constant --- icaruscode/PMT/Algorithms/pmtsimulation_icarus.fcl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/icaruscode/PMT/Algorithms/pmtsimulation_icarus.fcl b/icaruscode/PMT/Algorithms/pmtsimulation_icarus.fcl index fdc3298a9..7f79852c1 100644 --- a/icaruscode/PMT/Algorithms/pmtsimulation_icarus.fcl +++ b/icaruscode/PMT/Algorithms/pmtsimulation_icarus.fcl @@ -481,7 +481,7 @@ icarus_pmtsimulationalg_run2_2025: { TailSuppression: { Apply: true Epsilon: 0.25 # fitted on Run1/2 data (run 9384) - Tau: 250.0 # [ns] + Tau: 500.0 # [ns] } } # icarus_pmtsimulationalg_run2_2025 @@ -522,7 +522,7 @@ icarus_pmtsimulationalg_run4_2025: { TailSuppression: { Apply: true Epsilon: 0.30 # fitted on Run3/4 data (run 11816) - Tau: 250.0 # [ns] + Tau: 500.0 # [ns] } } # icarus_pmtsimulationalg_run4_2025 From abc279b0bafd844680d88e9452f592adbae26cc8 Mon Sep 17 00:00:00 2001 From: Matteo Vicenzi Date: Mon, 13 Jul 2026 22:26:26 -0400 Subject: [PATCH 05/10] test distance dependent survival --- .../PMT/Algorithms/PMTsimulationAlg.cxx | 63 ++++++++++++++++++- icaruscode/PMT/Algorithms/PMTsimulationAlg.h | 56 +++++++++++++++++ .../PMT/Algorithms/pmtsimulation_icarus.fcl | 10 ++- icaruscode/PMT/SimPMTIcarus_module.cc | 12 ++++ 4 files changed, 137 insertions(+), 4 deletions(-) diff --git a/icaruscode/PMT/Algorithms/PMTsimulationAlg.cxx b/icaruscode/PMT/Algorithms/PMTsimulationAlg.cxx index 0cc3dde33..1de8c932d 100644 --- a/icaruscode/PMT/Algorithms/PMTsimulationAlg.cxx +++ b/icaruscode/PMT/Algorithms/PMTsimulationAlg.cxx @@ -16,6 +16,9 @@ // LArSoft libraries #include "lardataalg/DetectorInfo/DetectorTimings.h" +#include "larcorealg/Geometry/WireReadoutGeom.h" +#include "larcorealg/Geometry/OpDetGeo.h" +#include "larcorealg/Geometry/geo_vectors_utils.h" // geo::vect::toPoint() #include "larcorealg/CoreUtils/counter.h" #include "larcorealg/CoreUtils/StdUtils.h" // util::begin(), util::end() @@ -221,7 +224,7 @@ auto icarus::opdet::PMTsimulationAlg::makeGainFluctuator(int channel) const if (fParams.useGainCalibDB && fParams.gainCalibProvider) { // DB Gaussian: per-channel SPE area and width from database // fNominalSPEArea is the integral of the SPR template, i.e. the mean area per PE - // fBiasConstant covers the bias in the integral definitions btw SPR template and official reco + // fBiasConstant covers the bias in the integral definitions btw SPR template and official reco (SBN-docdb-46261) double const speArea = fParams.gainCalibProvider->getSPEArea(channel); double const speFitWidth = fParams.gainCalibProvider->getSPEFitWidth(channel); // gainRatio = speArea / fNominalSPEArea: mean effective PEs per true PE @@ -300,8 +303,21 @@ auto icarus::opdet::PMTsimulationAlg::CreateFullWaveform photons_used->clear(); photons_used->SetChannel(channel); } + // distance-dependent survival S(d): PMT center cached per channel + bool const applySd = fParams.distanceSurvival.apply; + geo::Point_t const PMTcenter = applySd + ? fParams.wireReadoutGeom->OpDetGeoFromOpChannel(channel).GetCenter() + : geo::Point_t{}; + for(auto const& ph : photons) { - if (!KicksPhotoelectron()) continue; + if (applySd) { + // Bernoulli survival p = QE * S(d), same stream as KicksPhotoelectron + double const d_cm + = (PMTcenter - geo::vect::toPoint(ph.InitialPosition)).R(); + if (CLHEP::RandFlat::shoot(fParams.randomEngine) + >= fQE * distanceSurvival(d_cm)) continue; + } + else if (!KicksPhotoelectron()) continue; if (photons_used) photons_used->push_back(ph); // copy @@ -714,6 +730,16 @@ bool icarus::opdet::PMTsimulationAlg::KicksPhotoelectron() const { return CLHEP::RandFlat::shoot(fParams.randomEngine) < fQE; } +//------------------------------------------------------------------------------ +double icarus::opdet::PMTsimulationAlg::distanceSurvival(double d_cm) const +{ + auto const& edges = fParams.distanceSurvival.binEdges; + auto const it = std::upper_bound(edges.begin(), edges.end(), d_cm); + if ((it == edges.begin()) || (it == edges.end())) return 1.0; + return fParams.distanceSurvival.factors[std::distance(edges.begin(), it) - 1]; +} // icarus::opdet::PMTsimulationAlg::distanceSurvival() + + // ----------------------------------------------------------------------------- void icarus::opdet::PMTsimulationAlg::AddPhotoelectrons( PulseSampling_t const& pulse, Waveform_t& wave, tick const time_bin, @@ -1040,9 +1066,37 @@ icarus::opdet::PMTsimulationAlgMaker::PMTsimulationAlgMaker fBaseConfig.tailSuppression.epsilon = config.TailSuppression().Epsilon(); fBaseConfig.tailSuppression.tau = config.TailSuppression().Tau(); + // + // distance-dependent photon survival + // + fBaseConfig.distanceSurvival.apply = config.DistanceSurvival().Apply(); + fBaseConfig.distanceSurvival.binEdges = config.DistanceSurvival().BinEdges(); + fBaseConfig.distanceSurvival.factors = config.DistanceSurvival().Factors(); + // // parameter checks // + if (fBaseConfig.distanceSurvival.apply) { + auto const& ds = fBaseConfig.distanceSurvival; + if (ds.binEdges.size() != ds.factors.size() + 1) { + throw cet::exception("PMTsimulationAlg") + << "DistanceSurvival: BinEdges must have exactly one more entry than" + " Factors (got " << ds.binEdges.size() << " edges, " + << ds.factors.size() << " factors)\n"; + } + if (!std::is_sorted(ds.binEdges.begin(), ds.binEdges.end())) { + throw cet::exception("PMTsimulationAlg") + << "DistanceSurvival: BinEdges must be sorted in increasing order\n"; + } + for (double const f: ds.factors) { + if ((f < 0.0) || (f > 1.0)) { + throw cet::exception("PMTsimulationAlg") + << "DistanceSurvival: survival factors must be in [0, 1] (got " + << f << ")\n"; + } + } + } // distance survival checks + if (std::abs(fBaseConfig.pulsePolarity) != 1.0) { throw cet::exception("PMTsimulationAlg") << "Pulse polarity settings can be only +1.0 or -1.0 (got: " @@ -1073,6 +1127,7 @@ std::unique_ptr icarus::opdet::PMTsimulationAlgMaker::operator()( std::uint64_t beamGateTimestamp, detinfo::LArProperties const& larProp, + geo::WireReadoutGeom const& wireReadoutGeom, detinfo::DetectorClocksData const& clockData, icarusDB::PMTTimingCorrections const* timingDelays, icarusDB::PhotonCalibratorFromDB const* gainCalibProvider, @@ -1086,7 +1141,7 @@ icarus::opdet::PMTsimulationAlgMaker::operator()( { return std::make_unique(makeParams( beamGateTimestamp, - larProp, clockData, timingDelays, gainCalibProvider, + larProp, wireReadoutGeom, clockData, timingDelays, gainCalibProvider, SPRfunction, pedestalGenerator, mainRandomEngine, darkNoiseRandomEngine, elecNoiseRandomEngine, trackSelectedPhotons @@ -1099,6 +1154,7 @@ icarus::opdet::PMTsimulationAlgMaker::operator()( auto icarus::opdet::PMTsimulationAlgMaker::makeParams( std::uint64_t beamGateTimestamp, detinfo::LArProperties const& larProp, + geo::WireReadoutGeom const& wireReadoutGeom, detinfo::DetectorClocksData const& clockData, icarusDB::PMTTimingCorrections const* timingDelays, icarusDB::PhotonCalibratorFromDB const* gainCalibProvider, @@ -1122,6 +1178,7 @@ auto icarus::opdet::PMTsimulationAlgMaker::makeParams( params.beamGateTimestamp = beamGateTimestamp; params.larProp = &larProp; + params.wireReadoutGeom = &wireReadoutGeom; params.clockData = &clockData; params.detTimings = detinfo::makeDetectorTimings(params.clockData); params.timingDelays = timingDelays; diff --git a/icaruscode/PMT/Algorithms/PMTsimulationAlg.h b/icaruscode/PMT/Algorithms/PMTsimulationAlg.h index 05537e74d..309042c3a 100644 --- a/icaruscode/PMT/Algorithms/PMTsimulationAlg.h +++ b/icaruscode/PMT/Algorithms/PMTsimulationAlg.h @@ -28,6 +28,7 @@ // LArSoft libraries #include "lardataobj/RawData/OpDetWaveform.h" #include "lardataobj/Simulation/SimPhotons.h" +#include "larcorealg/Geometry/fwd.h" // geo::WireReadoutGeom #include "lardataalg/DetectorInfo/LArProperties.h" #include "lardataalg/DetectorInfo/DetectorClocksData.h" #include "lardataalg/DetectorInfo/DetectorTimings.h" @@ -541,6 +542,17 @@ class icarus::opdet::PMTsimulationAlg { float tau = 250.0f; ///< charge-state decay time constant [ns] }; + /// Distance-dependent photon survival: each scintillation + /// photon is kept with probability `QE x S(d)`, where `d` is the + /// straight-line distance from the photon emission point to the PMT + /// center. `factors[i]` applies to `d` in `[binEdges[i], binEdges[i+1])` + /// [cm]; survival is 1 outside the covered range. + struct DistanceSurvivalParams_t { + bool apply = false; + std::vector binEdges; ///< bin edges [cm], size = factors + 1 + std::vector factors; ///< survival probabilities, in [0, 1] + }; + /// @{ /// @name High level configuration parameters. @@ -568,6 +580,7 @@ class icarus::opdet::PMTsimulationAlg { float saturation; //equivalent to the number of p.e. that saturates the electronic signal PMTspecs_t PMTspecs; ///< PMT specifications. TailSuppressionParams_t tailSuppression; ///< Causal subtractive droop correction parameters. + DistanceSurvivalParams_t distanceSurvival; ///< Distance-dependent photon survival S(d). bool doGainFluctuations; ///< Whether to simulate gain fluctuations. bool useGainCalibDB; ///< Whether to use per-channel DB gain for fluctuations. bool doTimingDelays; ///< Whether to simulate timing delays. @@ -581,6 +594,9 @@ class icarus::opdet::PMTsimulationAlg { detinfo::LArProperties const* larProp = nullptr; ///< LarProperties service provider. + /// OpDet geometry mapping (required if `distanceSurvival.apply`). + geo::WireReadoutGeom const* wireReadoutGeom = nullptr; + detinfo::DetectorClocksData const* clockData = nullptr; /// Timing delays service interfacing with database @@ -674,6 +690,10 @@ class icarus::opdet::PMTsimulationAlg { template void printConfiguration(Stream&& out, std::string indent = "") const; + /// Whether the distance-dependent photon survival S(d) is enabled + /// (requires full `sim::SimPhotons` input: photon positions). + bool appliesDistanceSurvival() const + { return fParams.distanceSurvival.apply; } /// Manages the conversion between names and values of `DiscriminationAlgo`. static util::MultipleChoiceSelection const @@ -1008,6 +1028,10 @@ class icarus::opdet::PMTsimulationAlg { /// Returns a random response whether a photon generates a photoelectron. bool KicksPhotoelectron() const; + + /// Returns the distance survival probability S(d) for a photon travelling + /// a straight-line distance `d_cm` [cm] (1 outside the configured range). + double distanceSurvival(double d_cm) const; /// Returns the ADC range allowed for photoelectron saturation. std::pair saturationRange(ADCcount baseline) const; @@ -1096,6 +1120,28 @@ class icarus::opdet::PMTsimulationAlgMaker { }; // struct TailSuppressionConfig + struct DistanceSurvivalConfig { + using Name = fhicl::Name; + using Comment = fhicl::Comment; + + fhicl::Atom Apply { + Name("Apply"), + Comment("Enable distance-dependent photon survival S(d)"), + false + }; + fhicl::Sequence BinEdges { + Name("BinEdges"), + Comment("Photon-to-PMT distance bin edges [cm]; one more than Factors"), + std::vector{} + }; + fhicl::Sequence Factors { + Name("Factors"), + Comment("Survival probability per distance bin (S = 1 outside range)"), + std::vector{} + }; + + }; // struct DistanceSurvivalConfig + /// Main algorithm FHiCL configuration. struct Config { @@ -1182,6 +1228,14 @@ class icarus::opdet::PMTsimulationAlgMaker { Comment("PMT anode baseline-droop correction (causal subtractive model)") }; + // + // distance-dependent photon survival + // + fhicl::Table DistanceSurvival { + Name("DistanceSurvival"), + Comment("Distance-dependent scintillation photon survival S(d)") + }; + // // dark noise // @@ -1256,6 +1310,7 @@ class icarus::opdet::PMTsimulationAlgMaker { std::unique_ptr operator()( std::uint64_t beamGateTimestamp, detinfo::LArProperties const& larProp, + geo::WireReadoutGeom const& wireReadoutGeom, detinfo::DetectorClocksData const& detClocks, icarusDB::PMTTimingCorrections const* timingDelays, icarusDB::PhotonCalibratorFromDB const* gainCalibProvider, @@ -1289,6 +1344,7 @@ class icarus::opdet::PMTsimulationAlgMaker { PMTsimulationAlg::ConfigurationParameters_t makeParams( std::uint64_t beamGateTimestamp, detinfo::LArProperties const& larProp, + geo::WireReadoutGeom const& wireReadoutGeom, detinfo::DetectorClocksData const& clockData, icarusDB::PMTTimingCorrections const* timingDelays, icarusDB::PhotonCalibratorFromDB const* gainCalibProvider, diff --git a/icaruscode/PMT/Algorithms/pmtsimulation_icarus.fcl b/icaruscode/PMT/Algorithms/pmtsimulation_icarus.fcl index 7f79852c1..70ee89432 100644 --- a/icaruscode/PMT/Algorithms/pmtsimulation_icarus.fcl +++ b/icaruscode/PMT/Algorithms/pmtsimulation_icarus.fcl @@ -480,10 +480,18 @@ icarus_pmtsimulationalg_run2_2025: { TailSuppression: { Apply: true - Epsilon: 0.25 # fitted on Run1/2 data (run 9384) + Epsilon: 0.27 # refit on Run1/2 data with DistanceSurvival on (run 9384) Tau: 500.0 # [ns] } + # Distance-dependent scintillation photon survival S(d): each photon kept + # with probability QE x S(d), d = straight-line emission-point-to-PMT distance. + DistanceSurvival: { + Apply: true + BinEdges: [ 250.0, 300.0, 350.0, 400.0, 450.0 ] # [cm] + Factors: [ 0.535, 0.562, 0.671, 0.838 ] # S = 1 outside range + } + } # icarus_pmtsimulationalg_run2_2025 diff --git a/icaruscode/PMT/SimPMTIcarus_module.cc b/icaruscode/PMT/SimPMTIcarus_module.cc index dce586625..633c1d4d4 100644 --- a/icaruscode/PMT/SimPMTIcarus_module.cc +++ b/icaruscode/PMT/SimPMTIcarus_module.cc @@ -23,6 +23,7 @@ // LArSoft libraries #include "larcore/CoreUtils/ServiceUtil.h" +#include "larcore/Geometry/WireReadout.h" #include "lardata/DetectorInfoServices/DetectorClocksService.h" #include "lardata/DetectorInfoServices/LArPropertiesService.h" #include "lardataalg/DetectorInfo/DetectorTimings.h" @@ -490,6 +491,7 @@ SimPMTIcarus::SimPMTIcarus(Parameters const& config) auto PMTsimulator = makePMTsimulator( e.time().value(), // using the event generation time as beam time stamp *(lar::providerFrom()), + art::ServiceHandle()->Get(), clockData, fDoTimingDelays ? lar::providerFrom() : nullptr, fUseGainCalibDB ? art::ServiceHandle()->provider() : nullptr, @@ -515,6 +517,16 @@ SimPMTIcarus::SimPMTIcarus(Parameters const& config) // Prefer SimPhotons if available. // Make sure that there are parallel inputs for both formats; bool const useLitePhotons = !pmtVector.isValid(); + + // Distance-dependent survival needs photon positions: never silently + // skip the correction on the position-less SimPhotonsLite path. + if (useLitePhotons && PMTsimulator->appliesDistanceSurvival()) { + throw art::Exception(art::errors::Configuration) + << "DistanceSurvival is enabled but only sim::SimPhotonsLite input" + " is available ('" << fInputModuleName.encode() << "'):" + " photon positions are required. Run the photon propagation with" + " full sim::SimPhotons.\n"; + } // storage for the photons that are not used (but still required) std::vector fakePhotons; From e2614e2a47fc407fc2327ec17bd0a8504d542a48 Mon Sep 17 00:00:00 2001 From: Matteo Vicenzi Date: Tue, 21 Jul 2026 16:38:45 -0500 Subject: [PATCH 06/10] update run2 tailsupp and survival distance tune --- .../PMT/Algorithms/pmtsimulation_icarus.fcl | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/icaruscode/PMT/Algorithms/pmtsimulation_icarus.fcl b/icaruscode/PMT/Algorithms/pmtsimulation_icarus.fcl index 70ee89432..a8de5fd6b 100644 --- a/icaruscode/PMT/Algorithms/pmtsimulation_icarus.fcl +++ b/icaruscode/PMT/Algorithms/pmtsimulation_icarus.fcl @@ -45,7 +45,9 @@ # introduced configuration for new Run1/2 SPR (SPRisolHitRun9271) # introduced configuration for new Run3/4 SPR (SPRisolHitRun12715) # 20260618 (mvicenzi@bnl.gov) -# introduced tail suppression tune +# introduced tail suppression tune +# 20260722 (mvicenzi@bnl.gov) +# introduced distance-based survival probability #include "opticalproperties_icarus.fcl" @@ -480,16 +482,17 @@ icarus_pmtsimulationalg_run2_2025: { TailSuppression: { Apply: true - Epsilon: 0.27 # refit on Run1/2 data with DistanceSurvival on (run 9384) - Tau: 500.0 # [ns] + Epsilon: 0.27 # best fit (SBN-docdb-48511) + Tau: 500.0 # [ns] } # Distance-dependent scintillation photon survival S(d): each photon kept - # with probability QE x S(d), d = straight-line emission-point-to-PMT distance. + # with probability QE x S(d), S = 1 outside range + # Run2 tune from SBN-doc-48511 DistanceSurvival: { Apply: true - BinEdges: [ 250.0, 300.0, 350.0, 400.0, 450.0 ] # [cm] - Factors: [ 0.535, 0.562, 0.671, 0.838 ] # S = 1 outside range + BinEdges: [ 150, 200, 250, 300, 350, 400, 450 ] # [cm] + Factors: [ 0.97, 0.87, 0.82, 0.82, 0.82, 0.85 ] } } # icarus_pmtsimulationalg_run2_2025 @@ -529,8 +532,8 @@ icarus_pmtsimulationalg_run4_2025: { TailSuppression: { Apply: true - Epsilon: 0.30 # fitted on Run3/4 data (run 11816) - Tau: 500.0 # [ns] + Epsilon: 0.27 # fitted on Run3/4 data (run 11816) + Tau: 500.0 # [ns] } } # icarus_pmtsimulationalg_run4_2025 From d53e2b156ff2aebe597ed5109038584e0b8a78df Mon Sep 17 00:00:00 2001 From: Matteo Vicenzi Date: Mon, 27 Jul 2026 20:26:12 -0400 Subject: [PATCH 07/10] improve documentation, clarify currently missing run4 tune --- icaruscode/PMT/Algorithms/PMTsimulationAlg.h | 118 ++++++++++++++---- .../PMT/Algorithms/pmtsimulation_icarus.fcl | 20 ++- 2 files changed, 108 insertions(+), 30 deletions(-) diff --git a/icaruscode/PMT/Algorithms/PMTsimulationAlg.h b/icaruscode/PMT/Algorithms/PMTsimulationAlg.h index 309042c3a..970f677cd 100644 --- a/icaruscode/PMT/Algorithms/PMTsimulationAlg.h +++ b/icaruscode/PMT/Algorithms/PMTsimulationAlg.h @@ -164,10 +164,14 @@ class icarus::opdet::OpDetWaveformMakerClass { * The algorithm creates simulated PMT waveforms as read out by ICARUS, * including the generation of trigger primitives. * Contributions to the waveforms include: - * * physical photons + * * physical photons (optionally gated by a distance-dependent survival + * probability, see below) * * dark noise * * electronics noise - * * tail suppression + * + * In addition, an optional post-processing correction ("tail suppression", + * see below) may be applied to the signal waveform to reproduce the + * behaviour observed in data in the tail of large pulses. * * The algorithm processes an optical channel at a time, independently * and uncorrelated from the other channels. @@ -215,10 +219,22 @@ class icarus::opdet::OpDetWaveformMakerClass { * of the photon converting to a photoelectron, the quantum efficiency * check here should be skipped by setting the efficiency to 1. * + * When enabled by `DistanceSurvival.Apply`, the photoelectron conversion is + * additionally gated by a distance-dependent survival probability @f$ S(d) @f$: + * each photon is kept with total probability @f$ QE \times S(d) @f$, where + * @f$ d @f$ is the straight-line distance from the photon emission point to + * the PMT center. This is drawn from the same random stream as the plain + * quantum-efficiency acceptance, so the two are mutually exclusive at + * configuration time (either `KicksPhotoelectron()` or the @f$ QE \times S(d) @f$ + * gate is used, never both). @f$ S(d) @f$ is a piecewise-constant function of + * the distance (see the "Distance-dependent photon survival" section below). + * Since it requires the photon emission position, this feature needs the full + * `sim::SimPhotons` input (positions), not the position-less `sim::SimPhotonsLite`. + * * For each converting photon, a photoelectron is added to the channel by * placing a template waveform shape into the channel waveform. * - * If enabled by `ApplyTimingDelays`, the timing correction service + * If enabled by `ApplyTimingDelays`, the timing correction service * `icarusDB::IPMTTimingCorrectionService` is used to simulate the chain of * delays between the photon hitting the photocathode and the time its signal * is digitized. The delay is dominated by the signal cable length (~200ns). @@ -322,27 +338,62 @@ class icarus::opdet::OpDetWaveformMakerClass { * Noise can be disabled by using the `PMTnoNoiseGeneratorTool`. * * - * Tail suppression - * ----------------- + * Distance-dependent photon survival (SBN-docdb-48511) + * ---------------------------------------------------- + * + * When enabled via `DistanceSurvival.Apply`, each scintillation photon is + * accepted as a photoelectron with probability @f$ QE \times S(d) @f$ instead + * of the plain quantum-efficiency acceptance, where @f$ d @f$ is the + * straight-line distance [cm] from the photon emission point to the PMT + * center and @f$ S(d) \in [0, 1] @f$ is a survival factor. This provides an + * effective, distance-dependent correction to the light yield on top of the + * upstream photon propagation. + * + * @f$ S(d) @f$ is piecewise constant: it is defined by a set of bin edges + * (`DistanceSurvival.BinEdges`, sorted, in cm) and one survival factor per + * bin (`DistanceSurvival.Factors`, so `BinEdges` has exactly one more entry + * than `Factors`). `Factors[i]` applies to @f$ d \in + * [ \mathrm{BinEdges}[i], \mathrm{BinEdges}[i+1] ) @f$; for distances outside + * the covered range the survival is 1 (photon subject to plain `QE` only). + * The acceptance is drawn from the same random stream as the quantum + * efficiency, and the correction is uniform across all channels. + * + * Because the emission position is required, this feature is only available + * on the full `sim::SimPhotons` input path; if only the position-less + * `sim::SimPhotonsLite` is available the module raises a configuration error + * rather than silently dropping the correction (see `SimPMTIcarus`). * - * Data waveforms exhibit a baseline droop in the tail of large pulses: - * after a bright flash, the waveform sits systematically below what - * predicted by linearly scaling the SPE template. This effect is consistent with - * dynode space-charge saturation proportional to the recent integrated charge. * - * When enabled via `TailSuppression.Apply`, a low-pass filter is applied - * to the signal waveform (pre-pedestal) to accumulate the integrated charge. - * The filter is implemented as a recursive average: + * Tail suppression (SBN-docdb-48157, SBN-docdb-48511) + * --------------------------------------------------- + * + * In the tail of large pulses, data waveforms sit systematically below the + * level predicted by linearly scaling the SPE template. This correction is an + * empirical, tunable model to reproduce that behaviour in simulation. + * + * When enabled via `TailSuppression.Apply`, a low-pass filter is run over the + * signal waveform (pre-pedestal) to track the recent integrated charge. The + * filter is a recursive (exponential moving) average of the polarity-corrected + * samples: * @f[ * Q(n) = (1 - \beta)\,Q(n-1) + \beta\,\max(0,\,s(n)) * @f] - * where @\beta` is the inverse time constant of the filter (`TailSuppression.Tau`). - * The charge state is initialized to zero at the start of each - * channel's full waveform and does not persist across channels. - * - * Based on the accumulated charge, a correction is applied to the signal - * through a tunable strenght parameter `TailSuppression.Epsilon`. - * The parameters of the corrections are the same across all channels. + * where @f$ s(n) @f$ is the polarity-corrected sample and @f$ \beta @f$ is the + * filter smoothing factor. It is the inverse of the decay time constant + * `TailSuppression.Tau` expressed in ticks, i.e. + * @f$ \beta = 1 / (\tau \cdot f_{\mathrm{sampling}}) @f$ with @f$ \tau @f$ the + * `Tau` value [ns] and @f$ f_{\mathrm{sampling}} @f$ the sampling frequency + * [GHz, ticks/ns]. The charge state @f$ Q @f$ is initialized to zero at the start of + * each channel's full waveform and does not persist across channels. + * + * The tracked charge is then subtracted from each sample, scaled by a tunable + * strength parameter `TailSuppression.Epsilon`, with the result clamped so it + * does not cross the baseline: + * @f[ + * s'(n) = \max(0,\, s(n) - \varepsilon\,Q(n)) + * @f] + * (the polarity is restored before the sample is written back). The + * correction parameters are the same across all channels. * * Configuration * ============== @@ -371,12 +422,6 @@ class icarus::opdet::OpDetWaveformMakerClass { * @f$ \mu_{i} \propto (\Delta V_{i})^{k} @f$ (with @f$ \mu_{i} @f$ the * gain for stage @f$ i @f$, @f$ \Delta V_{i} @f$ the drop of potential * of that stage and @f$ k @f$ the parameter set by `dynodeK`. - * * `TailSuppression` (table, optional): parameters for the tail suppression - * correction (see "Tail suppression" section above). Sub-parameters: - * * `Apply` (default: `false`): enable the correction. - * * `Epsilon` (default: `0.0`): correction strength @f$ \varepsilon @f$ - * (dimensionless); typical values 0.25 (Run 1/2) or 0.30 (Run 3/4). - * * `Tau` (default: `250.0`): charge-state decay time constant [ns]. * * * `DiscrimAlgo` (choice, default: `"CrossingThreshold"`): selects one of the * hard-coded discrimination algorithms used for zero suppression. @@ -397,6 +442,29 @@ class icarus::opdet::OpDetWaveformMakerClass { * always contain just noise. * * + * Signal corrections + * ------------------ + * + * These optional top-level configuration tables control the corrections + * described in the "Distance-dependent photon survival" and "Tail suppression" + * sections above. Both are disabled by default. + * + * * `DistanceSurvival` (table, optional): distance-dependent photon survival + * @f$ S(d) @f$. Sub-parameters: + * * `Apply` (default: `false`): enable the correction. Requires full + * `sim::SimPhotons` input (photon positions). + * * `BinEdges` (default: empty): distance bin edges [cm], sorted; must have + * exactly one more entry than `Factors`. + * * `Factors` (default: empty): survival probability per bin, each in + * `[0, 1]`; survival is 1 for distances outside the covered range. + * * `TailSuppression` (table, optional): empirical tail suppression correction. + * Sub-parameters: + * * `Apply` (default: `false`): enable the correction. + * * `Epsilon` (default: `0.0`): correction strength @f$ \varepsilon @f$ + * (dimensionless); typical values 0.27 (Run 1/2). + * * `Tau` (default: `250.0`): charge-state decay time constant [ns]. + * + * * Random number generators * ------------------------- * diff --git a/icaruscode/PMT/Algorithms/pmtsimulation_icarus.fcl b/icaruscode/PMT/Algorithms/pmtsimulation_icarus.fcl index a8de5fd6b..aab91269f 100644 --- a/icaruscode/PMT/Algorithms/pmtsimulation_icarus.fcl +++ b/icaruscode/PMT/Algorithms/pmtsimulation_icarus.fcl @@ -45,9 +45,9 @@ # introduced configuration for new Run1/2 SPR (SPRisolHitRun9271) # introduced configuration for new Run3/4 SPR (SPRisolHitRun12715) # 20260618 (mvicenzi@bnl.gov) -# introduced tail suppression tune +# introduced tail suppression tune for run2 # 20260722 (mvicenzi@bnl.gov) -# introduced distance-based survival probability +# introduced distance-based survival probability for run2 #include "opticalproperties_icarus.fcl" @@ -480,9 +480,11 @@ icarus_pmtsimulationalg_run2_2025: { Gain: @local::icarus_settings_opdet_run2.nominalPMTgain # total PMT gain } + # Tail suppression effective correction + # Run2 tune from SBN-doc-48511 TailSuppression: { Apply: true - Epsilon: 0.27 # best fit (SBN-docdb-48511) + Epsilon: 0.27 # correction strength Tau: 500.0 # [ns] } @@ -530,12 +532,20 @@ icarus_pmtsimulationalg_run4_2025: { Gain: @local::icarus_settings_opdet_run4.nominalPMTgain # total PMT gain } + # TODO: requires tuning on Run-4 data TailSuppression: { - Apply: true - Epsilon: 0.27 # fitted on Run3/4 data (run 11816) + Apply: false + Epsilon: 0.27 Tau: 500.0 # [ns] } + # TODO: requires tuning on Run-4 data + DistanceSurvival: { + Apply: false + BinEdges: [ 150, 200, 250, 300, 350, 400, 450 ] # [cm] + Factors: [ 0.97, 0.87, 0.82, 0.82, 0.82, 0.85 ] + } + } # icarus_pmtsimulationalg_run4_2025 # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 448702abefc0d639af436b2726994957a48eb242 Mon Sep 17 00:00:00 2001 From: mvicenzi <64699641+mvicenzi@users.noreply.github.com> Date: Wed, 29 Jul 2026 00:05:34 -0400 Subject: [PATCH 08/10] Update icaruscode/PMT/Algorithms/PMTsimulationAlg.cxx Co-authored-by: Gianluca Petrillo --- icaruscode/PMT/Algorithms/PMTsimulationAlg.cxx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/icaruscode/PMT/Algorithms/PMTsimulationAlg.cxx b/icaruscode/PMT/Algorithms/PMTsimulationAlg.cxx index 1de8c932d..0e140acbc 100644 --- a/icaruscode/PMT/Algorithms/PMTsimulationAlg.cxx +++ b/icaruscode/PMT/Algorithms/PMTsimulationAlg.cxx @@ -305,9 +305,7 @@ auto icarus::opdet::PMTsimulationAlg::CreateFullWaveform } // distance-dependent survival S(d): PMT center cached per channel bool const applySd = fParams.distanceSurvival.apply; - geo::Point_t const PMTcenter = applySd - ? fParams.wireReadoutGeom->OpDetGeoFromOpChannel(channel).GetCenter() - : geo::Point_t{}; + geo::Point_t const PMTcenter = fParams.wireReadoutGeom->OpDetGeoFromOpChannel(channel).GetCenter(); for(auto const& ph : photons) { if (applySd) { From feec4d2697d19a65b5402081d1e23964111b5e1c Mon Sep 17 00:00:00 2001 From: mvicenzi <64699641+mvicenzi@users.noreply.github.com> Date: Wed, 29 Jul 2026 00:07:11 -0400 Subject: [PATCH 09/10] Apply suggestion from @PetrilloAtWork Co-authored-by: Gianluca Petrillo --- icaruscode/PMT/Algorithms/PMTsimulationAlg.h | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/icaruscode/PMT/Algorithms/PMTsimulationAlg.h b/icaruscode/PMT/Algorithms/PMTsimulationAlg.h index 970f677cd..d2fd6b09f 100644 --- a/icaruscode/PMT/Algorithms/PMTsimulationAlg.h +++ b/icaruscode/PMT/Algorithms/PMTsimulationAlg.h @@ -1514,10 +1514,9 @@ void icarus::opdet::PMTsimulationAlg::printConfiguration << '\n' << indent << "Gain at first stage: " << fParams.PMTspecs.firstStageGain() << '\n' << indent << "SPR nominal area: " << fNominalSPEArea << " ADC x tick" << '\n' << indent << "Bias ratio: " << fBiasConstant + << '\n' << indent << "Tail suppression: " + << std::boolalpha << fParams.tailSuppression.apply ; - - out << '\n' << indent << "Tail suppression: " - << std::boolalpha << fParams.tailSuppression.apply; if (fParams.tailSuppression.apply) { out << " (epsilon=" << fParams.tailSuppression.epsilon << ", tau=" << fParams.tailSuppression.tau << " ns)"; From 69b60a26563f9c8c85cf66e0349678b37d3edb70 Mon Sep 17 00:00:00 2001 From: Matteo Vicenzi Date: Wed, 29 Jul 2026 00:12:15 -0400 Subject: [PATCH 10/10] switch to fhicl::OptionalTable + print DistanceSurvival settings as well --- icaruscode/PMT/Algorithms/PMTsimulationAlg.cxx | 16 ++++++++++------ icaruscode/PMT/Algorithms/PMTsimulationAlg.h | 13 +++++++++++-- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/icaruscode/PMT/Algorithms/PMTsimulationAlg.cxx b/icaruscode/PMT/Algorithms/PMTsimulationAlg.cxx index 0e140acbc..8a1097e2f 100644 --- a/icaruscode/PMT/Algorithms/PMTsimulationAlg.cxx +++ b/icaruscode/PMT/Algorithms/PMTsimulationAlg.cxx @@ -1060,16 +1060,20 @@ icarus::opdet::PMTsimulationAlgMaker::PMTsimulationAlgMaker // // tail suppression // - fBaseConfig.tailSuppression.apply = config.TailSuppression().Apply(); - fBaseConfig.tailSuppression.epsilon = config.TailSuppression().Epsilon(); - fBaseConfig.tailSuppression.tau = config.TailSuppression().Tau(); + if (config.TailSuppression()) { + fBaseConfig.tailSuppression.apply = config.TailSuppression()->Apply(); + fBaseConfig.tailSuppression.epsilon = config.TailSuppression()->Epsilon(); + fBaseConfig.tailSuppression.tau = config.TailSuppression()->Tau(); + } // // distance-dependent photon survival // - fBaseConfig.distanceSurvival.apply = config.DistanceSurvival().Apply(); - fBaseConfig.distanceSurvival.binEdges = config.DistanceSurvival().BinEdges(); - fBaseConfig.distanceSurvival.factors = config.DistanceSurvival().Factors(); + if (config.DistanceSurvival()) { + fBaseConfig.distanceSurvival.apply = config.DistanceSurvival()->Apply(); + fBaseConfig.distanceSurvival.binEdges = config.DistanceSurvival()->BinEdges(); + fBaseConfig.distanceSurvival.factors = config.DistanceSurvival()->Factors(); + } // // parameter checks diff --git a/icaruscode/PMT/Algorithms/PMTsimulationAlg.h b/icaruscode/PMT/Algorithms/PMTsimulationAlg.h index d2fd6b09f..df4786736 100644 --- a/icaruscode/PMT/Algorithms/PMTsimulationAlg.h +++ b/icaruscode/PMT/Algorithms/PMTsimulationAlg.h @@ -44,6 +44,7 @@ #include "messagefacility/MessageLogger/MessageLogger.h" #include "fhiclcpp/types/Atom.h" #include "fhiclcpp/types/OptionalAtom.h" +#include "fhiclcpp/types/OptionalTable.h" #include "fhiclcpp/types/Sequence.h" #include "fhiclcpp/types/Table.h" @@ -1291,7 +1292,7 @@ class icarus::opdet::PMTsimulationAlgMaker { // // tail suppression // - fhicl::Table TailSuppression { + fhicl::OptionalTable TailSuppression { Name("TailSuppression"), Comment("PMT anode baseline-droop correction (causal subtractive model)") }; @@ -1299,7 +1300,7 @@ class icarus::opdet::PMTsimulationAlgMaker { // // distance-dependent photon survival // - fhicl::Table DistanceSurvival { + fhicl::OptionalTable DistanceSurvival { Name("DistanceSurvival"), Comment("Distance-dependent scintillation photon survival S(d)") }; @@ -1521,6 +1522,14 @@ void icarus::opdet::PMTsimulationAlg::printConfiguration out << " (epsilon=" << fParams.tailSuppression.epsilon << ", tau=" << fParams.tailSuppression.tau << " ns)"; } + out + << '\n' << indent << "Distance survival: " + << std::boolalpha << fParams.distanceSurvival.apply + ; + if (fParams.distanceSurvival.apply) { + out << " (" << fParams.distanceSurvival.factors.size() << " bins over " + << fParams.distanceSurvival.binEdges.size() << " edges)"; + } out << '\n' << indent << "Pedestal: " << fPedestalGen->toString(indent + " ", "");