All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Groups Pages
PMTsimulationAlg.cxx
Go to the documentation of this file.
1 /**
2  * @file icaruscode/PMT/Algorithms/PMTsimulationAlg.cxx
3  * @brief Algorithms for the simulation of ICARUS PMT channels.
4  * @date October 16, 2018
5  * @see `icaruscode/PMT/Algorithms/PMTsimulationAlg.h`
6  *
7  * Implementation file.
8  */
9 
10 // this library header
12 
13 // ICARUS libraries
16 
17 
18 // LArSoft libraries
21 #include "larcorealg/CoreUtils/StdUtils.h" // util::begin(), util::end()
22 
23 // framework libraries
24 #include "cetlib_except/exception.h"
25 
26 // CLHEP libraries
27 #include "CLHEP/Random/RandFlat.h"
28 #include "CLHEP/Random/RandPoisson.h"
29 #include "CLHEP/Random/RandGaussQ.h"
30 #include "CLHEP/Random/RandExponential.h"
31 
32 // C++ standard libaries
33 #include <chrono> // std::chrono::high_resolution_clock
34 #include <unordered_map>
35 #include <algorithm>
36 #include <utility> // std::move(), std::cref(), ...
37 #include <limits> // std::numeric_limits
38 #include <cmath> // std::signbit(), std::pow()
39 #include <numeric> // std::accumulate
40 
41 
42 // -----------------------------------------------------------------------------
43 #if __cplusplus < 202002L // C++20?
44 namespace util {
45 
46  /// Substitute for C++20 `std::identity`.
47  struct identity {
48  template <typename T>
49  constexpr T&& operator() (T&& v) const noexcept
50  { return std::forward<T>(v); }
51  }; // struct identity
52 
53 } // namespace util
54 #else
55 # error("Replace util::identity with std::identity (#include <functional>)")
56 #endif
57 
58 
59 // -----------------------------------------------------------------------------
60 // --- icarus::opdet::PMTsimulationAlg
61 // -----------------------------------------------------------------------------
62 
63 // The reason for this being static is feeble... in short: because it can be;
64 // the memory it takes is already "static" anyway, so no gain in there.
65 // At least, this clarifies the absolute nature of this object.
68 
69 
70 // -----------------------------------------------------------------------------
71 double
73  multiplicationStageGain(unsigned int i /* = 1 */) const
74 {
75  // if all stages were born equal:
76  // return std::pow(gain, 1.0 / nDynodes());
77  double const k = dynodeK;
78  double const mu = gain;
79  unsigned const N = nDynodes();
80  double prodRho = 1.0;
81  for (double rho: voltageDistribution) prodRho *= rho;
82  double const aVk
83  = std::pow(mu / std::pow(prodRho, k), 1.0/static_cast<double>(N));
84  return aVk * std::pow(voltageDistribution.at(i - 1), k);
85 } // icarus::...::PMTsimulationAlg::...::PMTspecs_t::multiplicationStageGain()
86 
87 
88 // -----------------------------------------------------------------------------
91  (std::vector<double>&& Rs)
92 {
93  voltageDistribution = std::move(Rs);
94  double total = std::accumulate
95  (voltageDistribution.begin(), voltageDistribution.end(), 0.0);
96  for (auto& R: voltageDistribution) R /= total;
97 } // icarus::...::PMTsimulationAlg::...::PMTspecs_t::setPMTvoltageDistribution()
98 
99 
100 // -----------------------------------------------------------------------------
103  : fParams(config)
106  , fNsamples(fParams.readoutEnablePeriod * fSampling) // us * MHz cancels out
107  , wsp( // NOTE: wsp amplitude already includes sign from polarity
109  fSampling,
110  fParams.pulseSubsamples, // tick subsampling
111  1.0e-4_ADCf // stop sampling when ADC counts are below this value
112  )
116  )
117 {
118  using namespace util::quantities::electronics_literals;
119 
120  // mf::LogDebug("PMTsimulationAlg") << "Sampling = " << fSampling << std::endl;
121 
122  // shape of single pulse
123 
124  // Correction due to scalling factor applied during simulation if any
125  mf::LogDebug("PMTsimulationAlg") << "PMT corrected efficiency = " << fQE;
126 
127  if (fQE >= 1.0001) {
128  mf::LogWarning("PMTsimulationAlg")
129  << "WARNING: Quantum efficiency set in the configuration (QE="
130  << fParams.QEbase << ") seems to be too large!"
131  "\nThe photon visibility library is assumed to already include a"
132  " quantum efficiency of " << fParams.larProp->ScintPreScale() <<
133  " (`ScintPreScale` setting of `LArProperties` service)"
134  " and here we are requesting a higher one."
135  "\nThis configuration is not supported by `PMTsimulationAlg`,"
136  " and this simulation will effectively apply a quantum efficiency of "
138  ;
139  }
140 
141  // check that the sampled waveform has a sufficiently large range, so that
142  // tails are below 10^-3 ADC counts (absolute value);
143  // if this test fails, it's better to reduce the threshold in wsp constructor
144  // (for analytical pulses converging to 0) or have a longer sampling that does
145  // converge to 0 (10^-3 ADC is quite low though).
146  wsp.checkRange(1.0e-3_ADCf, "PMTsimulationAlg");
147 
148 } // icarus::opdet::PMTsimulationAlg::PMTsimulationAlg()
149 
150 
151 // -----------------------------------------------------------------------------
152 std::tuple<std::vector<raw::OpDetWaveform>, std::optional<sim::SimPhotons>>
154  sim::SimPhotonsLite const& lite_photons)
155 {
156  std::optional<sim::SimPhotons> photons_used;
157 
158  Waveform_t const waveform = CreateFullWaveform(photons, lite_photons, photons_used);
159 
160  return {
161  CreateFixedSizeOpDetWaveforms(photons.OpChannel(), waveform),
162  std::move(photons_used)
163  };
164 
165 } // icarus::opdet::PMTsimulationAlg::simulate()
166 
167 
168 //------------------------------------------------------------------------------
170 
171  using Fluctuator_t = GainFluctuator<CLHEP::RandPoisson>;
172 
174  double const refGain = fParams.PMTspecs.firstStageGain();
175  return Fluctuator_t
176  { refGain, CLHEP::RandPoisson{ *fParams.gainRandomEngine, refGain } };
177  }
178  else return Fluctuator_t{}; // default-constructed does not fluctuate anything
179 
180 } // icarus::opdet::PMTsimulationAlg::makeGainFluctuator()
181 
182 
183 //------------------------------------------------------------------------------
185  (sim::SimPhotons const& photons,
186  sim::SimPhotonsLite const& lite_photons,
187  std::optional<sim::SimPhotons>& photons_used)
188  const -> Waveform_t
189 {
190 
191  using namespace util::quantities::time_literals;
192  using namespace util::quantities::frequency_literals;
193  using namespace util::quantities::electronics_literals;
194  using namespace detinfo::timescales;
195 
196  detinfo::DetectorTimings const& timings
198 
199  tick const endSample = tick::castFrom(fNsamples);
200 
201  //
202  // collect the amount of photoelectrons arriving at each subtick;
203  // the waveform is split in groups of photons at the same relative subtick
204  // (i.e. first all the photons on the first subtick of a tick, then
205  // all the photons on the second subtick of a tick, and so on);
206  // storage is by subtick group (vector index is the subtick), then by
207  // tick (unordered map index is the tick number).
208  //
209  std::vector<std::unordered_map<tick, unsigned int>> peMaps
210  (wsp.nSubsamples());
211 
212  // returns tick and relative subtick number
213  TimeToTickAndSubtickConverter const toTickAndSubtick(peMaps.size());
214 
215 // auto start = std::chrono::high_resolution_clock::now();
216 
217  if (photons_used) {
218  photons_used->clear();
219  photons_used->SetChannel(photons.OpChannel());
220  }
221  for(auto const& ph : photons) {
222  if (!KicksPhotoelectron()) continue;
223 
224  if (photons_used) photons_used->push_back(ph); // copy
225 
226  simulation_time const photonTime { ph.Time };
227 
228  trigger_time const mytime
229  = timings.toTriggerTime(photonTime)
231  ;
232  if ((mytime < 0.0_us) || (mytime >= fParams.readoutEnablePeriod)) continue;
233 
234  auto const [ tick, subtick ]
235  = toTickAndSubtick(mytime.quantity() * fSampling);
236  /*
237  mf::LogTrace("PMTsimulationAlg")
238  << "Photon at " << photonTime << ", optical time " << mytime
239  << " => tick " << tick_d
240  << " => sample " << tick << " subsample " << subtick
241  ;
242  */
243  if (tick >= endSample) continue;
244  ++peMaps[subtick][tick];
245  } // for photons
246 
247 // auto end = std::chrono::high_resolution_clock::now();
248 // std::chrono::duration<double> diff = end-start;
249 // std::cout << "\tcollected pes... " << photons.OpChannel() << " " << diff.count() << std::endl;
250 // start=std::chrono::high_resolution_clock::now();
251 
252  // Add SimPhotonsLite. Loop over geant ticks (=ns).
253 
254  for(auto const& [ time_ns, nphotons ]: lite_photons.DetectedPhotons) {
255 
256  // Count photoelectrons.
257 
258  unsigned int nPE = 0;
259  for(int i=0; i<nphotons; ++i) {
260  if(KicksPhotoelectron())
261  ++nPE;
262  }
263 
264  // Convert photon time bin to ticks.
265 
266  simulation_time const photonTime { time_ns + 0.5 };
267  trigger_time const mytime
268  = timings.toTriggerTime(photonTime)
270  if ((mytime < 0.0_us) || (mytime >= fParams.readoutEnablePeriod)) continue;
271 
272  auto const [ tick, subtick ]
273  = toTickAndSubtick(mytime.quantity() * fSampling);
274  if (tick < endSample)
275  peMaps[subtick][tick] += nPE;
276  }
277 
278  //
279  // add the collected photoelectrons to the waveform
280  //
282 
283  unsigned int nTotalPE [[gnu::unused]] = 0U; // unused if not in `debug` mode
284  double nTotalEffectivePE [[gnu::unused]] = 0U; // unused if not in `debug` mode
285 
286  auto gainFluctuation = makeGainFluctuator();
287 
288  // go though all subsamples (starting each at a fraction of a tick)
289  for (auto const& [ iSubsample, peMap ]: util::enumerate(peMaps)) {
290 
291  // this is the waveform sampling for the selected subsample:
292  auto const& subsample = wsp.subsample(iSubsample);
293 
294  for (auto const& [ startTick, nPE ]: peMap) {
295  nTotalPE += nPE;
296 
297  double const nEffectivePE = gainFluctuation(nPE);
298  nTotalEffectivePE += nEffectivePE;
299 
301  subsample, waveform, startTick,
302  static_cast<WaveformValue_t>(nEffectivePE)
303  );
304  // std::cout << "Channel=" << photons.OpChannel() << ", subsample=" << iSubsample << ", tick=" << startTick << ", nPE=" << nPE << ", ePE=" << nEffectivePE << std::endl;
305 
306  } // for sample
307  } // for subsamples
308  MF_LOG_TRACE("PMTsimulationAlg")
309  << nTotalPE << " photoelectrons at "
310  << std::accumulate(
311  peMaps.begin(), peMaps.end(), 0U,
312  [](unsigned int n, auto const& map){ return n + map.size(); }
313  )
314  << " times in channel " << photons.OpChannel()
315  ;
316 
317 // end=std::chrono::high_resolution_clock::now(); diff = end-start;
318 // std::cout << "\tadded pes... " << photons.OpChannel() << " " << diff.count() << std::endl;
319 // start=std::chrono::high_resolution_clock::now();
320 
321  if(fParams.ampNoise > 0.0_ADCf) (this->*fNoiseAdder)(waveform);
322  if(fParams.darkNoiseRate > 0.0_Hz) AddDarkNoise(waveform);
323 
324 // end=std::chrono::high_resolution_clock::now(); diff = end-start;
325 // std::cout << "\tadded noise... " << photons.OpChannel() << " " << diff.count() << std::endl;
326 // start=std::chrono::high_resolution_clock::now();
327 
328  // saturation in terms of photoelectrons (sharp);
329  auto const ADCrange = fParams.ADCrange();
330  ApplySaturation(waveform, ADCrange);
331 
332  // clip to the ADC range, 0 -- 2
333  ClipWaveform(waveform, ADCrange.first, ADCrange.second);
334 
335 // end=std::chrono::high_resolution_clock::now(); diff = end-start;
336 // std::cout << "\tadded saturation... " << photons.OpChannel() << " " << diff.count() << std::endl;
337 
338  return waveform;
339  } // CreateFullWaveform()
340 
342  -> std::vector<optical_tick>
343  {
344  using namespace util::quantities::time_literals;
346 
347  detinfo::DetectorTimings const& timings
349 
350  std::vector<optical_tick> trigger_locations;
351  trigger_locations.reserve(fParams.beamGateTriggerNReps);
352  trigger_time const beamTime = timings.toTriggerTime(timings.BeamGateTime());
353 
354  for(unsigned int i_trig=0; i_trig<fParams.beamGateTriggerNReps; ++i_trig) {
355  trigger_time const trig_time
356  = beamTime
359  ;
360  if (trig_time < 0_us) continue;
361  if (trig_time > fParams.readoutEnablePeriod) break;
362  trigger_locations.push_back
363  (optical_tick::castFrom(trig_time.quantity()*fSampling));
364  }
365  return trigger_locations;
366  }
367 
369  -> std::vector<optical_tick>
370  {
371  std::vector<optical_tick> trigger_locations;
372 
373  // find all ticks at which we would trigger readout
374  bool above_thresh=false;
375  for(size_t i_t=0; i_t<wvfm.size(); ++i_t){
376 
377  auto const val { fParams.pulsePolarity* (wvfm[i_t]-fParams.baseline) };
378 
379  if(!above_thresh && val>=fParams.thresholdADC){
380  above_thresh=true;
381  trigger_locations.push_back(optical_tick::castFrom(i_t));
382  }
383  else if(above_thresh && val<fParams.thresholdADC){
384  above_thresh=false;
385  }
386 
387  }//end loop over waveform
388 
389  // next, add the triggers injected at beam gate time
391  auto beamGateTriggers = CreateBeamGateTriggers();
392 
393  // insert the new triggers and sort them
394  trigger_locations.insert(trigger_locations.end(),
395  beamGateTriggers.begin(), beamGateTriggers.end());
396  std::inplace_merge(
397  trigger_locations.begin(),
398  trigger_locations.end() - beamGateTriggers.size(),
399  trigger_locations.end()
400  );
401  }
402 
403  return trigger_locations;
404  }
405 
406 //------------------------------------------------------------------------------
407 std::vector<raw::OpDetWaveform>
409  (raw::Channel_t opChannel, Waveform_t const& waveform) const
410 {
411  /*
412  * Plan:
413  *
414  * 1. set up
415  * 2. get the trigger points of the waveform
416  * 3. define the size of data around each trigger to commit to waveforms
417  * (also merge contiguous and overlapping intervals)
418  * 4. create the actual `raw::OpDetWaveform` objects
419  *
420  */
421 
422  //
423  // parameters check and setup
424  //
425 
426  // not a big deal if this assertion fails, but a bit more care needs to be
427  // taken in comparisons and subtractions
428  static_assert(
429  std::is_signed_v<optical_tick::value_t>,
430  "This algorithm requires tick type to be signed."
431  );
432 
433  using namespace detinfo::timescales; // electronics_time, time_interval, ...
434 
435  auto const pretrigSize = optical_time_ticks::castFrom(fParams.pretrigSize());
436  auto const posttrigSize = optical_time_ticks::castFrom(fParams.posttrigSize());
437 
438  detinfo::DetectorTimings const& timings
440 
441  // first viable tick number: since this is the item index in `wvfm`, it's 0
442  optical_tick const firstTick { 0 };
443 
444  // use hardware trigger time plus the configured offset as waveform start time
445  OpDetWaveformMaker_t createOpDetWaveform {
446  waveform,
447  timings.TriggerTime() + time_interval{ fParams.triggerOffsetPMT },
448  1.0 / fSampling
449  };
450 
451  //
452  // get the PMT channel triggers to consider
453  //
454 
455  // prepare the set of triggers
456  std::vector<optical_tick> const trigger_locations = FindTriggers(waveform);
457  auto const tend = trigger_locations.end();
458 
459  // find the first viable trigger
460  auto tooEarlyTrigger = [earliest = firstTick + pretrigSize](optical_tick t)
461  { return t < earliest; };
462  auto iNextTrigger
463  = std::find_if_not(trigger_locations.begin(), tend, tooEarlyTrigger);
464 
465  //
466  // collect all buffer ranges and merge them
467  //
468  using BufferRange_t = OpDetWaveformMaker_t::BufferRange_t;
469  auto makeBuffer
470  = [pretrigSize, posttrigSize](optical_tick triggerTime) -> BufferRange_t
471  { return { triggerTime - pretrigSize, triggerTime + posttrigSize }; }
472  ;
473 
474  std::vector<BufferRange_t> buffers;
475  buffers.reserve(std::distance(iNextTrigger, tend)); // worst case
476 
477  auto earliestBufferStart { firstTick };
478  while (iNextTrigger != tend) {
479 
480  BufferRange_t const buffer = makeBuffer(*iNextTrigger);
481 
482  if (buffer.first < earliestBufferStart) { // extend the previous buffer
483  assert(!buffers.empty()); // guaranteed because we skipped early triggers
484  buffers.back().second = buffer.second;
485  }
486  else buffers.emplace_back(buffer);
487 
488  earliestBufferStart = buffer.second;
489 
490  ++iNextTrigger;
491  } // while
492 
493  //
494  // turn each buffer into a waveform
495  //
496  MF_LOG_TRACE("PMTsimulationAlg")
497  << "Channel #" << opChannel << ": " << buffers.size() << " waveforms for "
498  << trigger_locations.size() << " triggers"
499  ;
500  std::vector<raw::OpDetWaveform> output_opdets;
501  for (BufferRange_t const& buffer: buffers) {
502 
503  output_opdets.push_back(createOpDetWaveform(opChannel, buffer));
504 
505  } // for buffers
506 
507  return output_opdets;
508 } // icarus::opdet::PMTsimulationAlg::CreateFixedSizeOpDetWaveforms()
509 
510 
511 // -----------------------------------------------------------------------------
513  { return CLHEP::RandFlat::shoot(fParams.randomEngine) < fQE; }
514 
515 
516 // -----------------------------------------------------------------------------
518  PulseSampling_t const& pulse, Waveform_t& wave, tick const time_bin,
519  WaveformValue_t const n
520 ) const {
521 
522  if (n == 0.0) return;
523 
524  if (n == 1.0) {
525  // simple addition
526  AddPulseShape(pulse, wave, time_bin, std::plus<ADCcount>());
527  }
528  else {
529  // multiply each `pulse` sample by `n`:
530  AddPulseShape(pulse, wave, time_bin, [n](auto a, auto b) { return a+n*b; });
531  }
532 
533 } // icarus::opdet::PMTsimulationAlg::AddPhotoelectrons()
534 
535 
536 // -----------------------------------------------------------------------------
537 template <typename Combine>
539  PulseSampling_t const& pulse, Waveform_t& wave, tick const time_bin,
540  Combine combination
541  ) const
542 {
543  std::size_t const min = time_bin.value();
544  std::size_t const max = std::min(min + pulse.size(), fNsamples);
545  if (min >= max) return;
546 
548  wave.begin() + min, wave.begin() + max,
549  pulse.begin(),
550  wave.begin() + min,
551  combination
552  );
553 
554 } // icarus::opdet::PMTsimulationAlg::AddPulseShape()
555 
556 
557 // -----------------------------------------------------------------------------
559 
560  CLHEP::RandGaussQ random
562  for(auto& sample: wave) {
563  ADCcount const noise { static_cast<float>(random.fire()) }; // Gaussian noise
564  sample += noise;
565  } // for sample
566 
567 } // PMTsimulationAlg::AddNoise()
568 
569 
570 // -----------------------------------------------------------------------------
572 
573  /*
574  * Compared to AddNoise(), we use a somehow faster random generator;
575  * to squeeze the CPU cycles, we avoid the CLHEP interface as much as
576  * possible; the random number from the engine is immediately converted
577  * to single precision, and the rest of the math happens in there as well.
578  * No virtual interfaces nor indirection is involved within this function
579  * (except for CLHEP random engine). We generate a normal variable _z_
580  * (standard deviation 1, mean 0) and we just scale it to the desired
581  * standard deviation, not bothering to add the mean offset of 0.
582  * Note that unless the random engine is multi-thread safe, this function
583  * won't gain anything from multi-threading.
584  */
585  auto& engine = *fParams.elecNoiseRandomEngine;
586 
587  for(auto& sample: wave) {
588  sample += fParams.ampNoise * fFastGauss(engine.flat()); // Gaussian noise
589  } // for sample
590 
591 } // PMTsimulationAlg::AddNoise_faster()
592 
593 
594 // -----------------------------------------------------------------------------
596  /*
597  * We assume leakage current ("dark noise") is completely stochastic and
598  * distributed uniformly in time with a fixed and known rate.
599  *
600  * In these condition, the time between two consecutive events follows an
601  * exponential distribution with as decay constant the same rate.
602  * We extact all the leakage events (including the first one) according to
603  * that distribution.
604  *
605  * We follow the "standard" approach of subsampling of tick as for the
606  * photoelectron.
607  *
608  */
609  using namespace util::quantities::frequency_literals;
610 
611  if (fParams.darkNoiseRate <= 0.0_Hz) return; // no dark noise
612 
613  // CLHEP random objects do not understand quantities, so we use scalars;
614  // we choose to work with nanosecond
615  CLHEP::RandExponential random(*(fParams.darkNoiseRandomEngine),
616  (1.0 / fParams.darkNoiseRate).convertInto<nanoseconds>().value());
617 
618  // time to stop at: full duration of the waveform
619  nanoseconds const maxTime = static_cast<double>(wave.size()) / fSampling;
620 
621  // the time of first leakage event:
622  nanoseconds darkNoiseTime { random.fire() };
623 
624  TimeToTickAndSubtickConverter const toTickAndSubtick(wsp.nSubsamples());
625 
626  auto gainFluctuation = makeGainFluctuator();
627 
628  MF_LOG_TRACE("PMTsimulationAlg")
629  << "Adding dark noise (" << fParams.darkNoiseRate << ") up to " << maxTime;
630 
631  while (darkNoiseTime < maxTime) {
632 
633  auto const [ tick, subtick ] = toTickAndSubtick(darkNoiseTime * fSampling);
634 
635  double const n = gainFluctuation(1.0); // leakage is one photoelectron
636  MF_LOG_TRACE("PMTsimulationAlg")
637  << " * at " << darkNoiseTime << " (" << tick << ", subsample " << subtick
638  << ") x" << n;
639 
641  (wsp.subsample(subtick), wave, tick, static_cast<WaveformValue_t>(n));
642 
643  // time of the next leakage event:
644  darkNoiseTime += nanoseconds{ random.fire() };
645 
646  } // while
647 
648 } // icarus::opdet::PMTsimulationAlg::AddDarkNoise()
649 
650 
651 
652 // -----------------------------------------------------------------------------
654  -> std::pair<ADCcount, ADCcount>
655 {
656  std::pair<ADCcount, ADCcount> range = fParams.ADCrange();
657  if (fParams.saturation > 0) {
658  ADCcount const saturationLevel
660  ((fParams.pulsePolarity > 0)? range.second: range.first) = saturationLevel;
661  }
662  return range;
663 } // icarus::opdet::PMTsimulationAlg::saturationRange()
664 
665 
666 // -----------------------------------------------------------------------------
668  (Waveform_t& waveform) const
669 {
670  //
671  // simple sharp capping/cupping of ADC values
672  //
673  auto const boundaries = saturationRange();
674  ClipWaveform(waveform, boundaries.first, boundaries.second);
675 
676 } // icarus::opdet::PMTsimulationAlg::ApplySaturation()
677 
678 
679 // -----------------------------------------------------------------------------
681  (Waveform_t& waveform, std::pair<ADCcount, ADCcount> const& range) const
682 {
683  //
684  // simple sharp capping/cupping of ADC values
685  //
686  auto const boundaries = saturationRange();
687 
688  // saturation is out of boundaries: do not apply it.
689  if ((boundaries.first <= range.first) && (boundaries.second >= range.second))
690  return;
691 
692  ClipWaveform(waveform, boundaries.first, boundaries.second);
693 
694 } // icarus::opdet::PMTsimulationAlg::ApplySaturation(range)
695 
696 
697 // -----------------------------------------------------------------------------
698 /// Forces `waveform` ADC within the `min` to `max` range (`max` included).
700  (Waveform_t& waveform, ADCcount min, ADCcount max)
701 {
702  using ClamperFunc_t = std::function<ADCcount(ADCcount)>;
703  ClamperFunc_t const clamper =
704  (min == std::numeric_limits<ADCcount>::min())
705  ? ((max == std::numeric_limits<ADCcount>::max())
706  ? ClamperFunc_t{ [](ADCcount s){ return s; } }
707  : ClamperFunc_t{ [max](ADCcount s){ return std::min(s, max); } }
708  )
709  : ((max == std::numeric_limits<ADCcount>::max())
710  ? ClamperFunc_t{ [min](ADCcount s){ return std::max(s, min); } }
711  : ClamperFunc_t{ [min,max](ADCcount s){ return std::clamp(s, min, max); } }
712  )
713  ;
714 
715  std::transform(waveform.cbegin(), waveform.cend(), waveform.begin(), clamper);
716 
717 } // icarus::opdet::PMTsimulationAlg::ClipWaveform()
718 
719 
720 // -----------------------------------------------------------------------------
721 auto icarus::opdet::PMTsimulationAlg::TimeToTickAndSubtickConverter::operator()
722  (double const tick_d) const -> std::tuple<tick, SubsampleIndex_t>
723 {
724  double const tickNumber_d = std::floor(tick_d);
725  double const subtick = std::floor((tick_d - tickNumber_d) * fNSubsamples);
726  return {
727  tick::castFrom(tickNumber_d),
728  static_cast<SubsampleIndex_t>(subtick)
729  };
730 } // icarus::opdet::PMTsimulationAlg::TimeToTickAndSubtickConverter::operator()
731 
732 
733 // -----------------------------------------------------------------------------
734 template <typename Rand>
736  (double const n)
737 {
738  return fRandomGain
739  ? (fRandomGain->fire(n * fReferenceGain) / fReferenceGain): n;
740 }
741 
742 
743 // -----------------------------------------------------------------------------
744 // --- icarus::opdet::PMTsimulationAlgMaker
745 // -----------------------------------------------------------------------------
747  (Config const& config)
748 {
755 
756  //
757  // readout settings
758  //
759  fBaseConfig.readoutEnablePeriod = config.ReadoutEnablePeriod();
760  fBaseConfig.readoutWindowSize = config.ReadoutWindowSize();
761  fBaseConfig.baseline = ADCcount(config.Baseline());
762  fBaseConfig.ADCbits = config.ADCBits();
763  fBaseConfig.pulsePolarity = config.PulsePolarity();
764  fBaseConfig.pretrigFraction = config.PreTrigFraction();
765  fBaseConfig.triggerOffsetPMT = config.TriggerOffsetPMT();
766 
767  //
768  // PMT settings
769  //
770  auto const& PMTspecs = config.PMTspecs();
771  fBaseConfig.saturation = config.Saturation().value_or(0);
772  fBaseConfig.QEbase = config.QE();
773  fBaseConfig.PMTspecs.dynodeK = PMTspecs.DynodeK();
774  fBaseConfig.PMTspecs.setVoltageDistribution
775  (PMTspecs.VoltageDistribution());
776  fBaseConfig.PMTspecs.gain = PMTspecs.Gain();
777  fBaseConfig.doGainFluctuations = config.FluctuateGain();
778 
779  //
780  // single photoelectron response
781  //
782  fBaseConfig.pulseSubsamples = config.PulseSubsamples();
783 
784  //
785  // dark noise
786  //
787  fBaseConfig.darkNoiseRate = hertz(config.DarkNoiseRate());
788 
789  //
790  // electronics noise
791  //
792  fBaseConfig.ampNoise = ADCcount(config.AmpNoise());
793  fBaseConfig.useFastElectronicsNoise = config.FastElectronicsNoise();
794 
795  //
796  // trigger
797  //
798  fBaseConfig.thresholdADC = ADCcount(config.ThresholdADC());
799  fBaseConfig.createBeamGateTriggers = config.CreateBeamGateTriggers();
800  fBaseConfig.beamGateTriggerRepPeriod = microsecond(config.BeamGateTriggerRepPeriod());
801  fBaseConfig.beamGateTriggerNReps = config.BeamGateTriggerNReps();
802 
803  //
804  // parameter checks
805  //
806  if (std::abs(fBaseConfig.pulsePolarity) != 1.0) {
807  throw cet::exception("PMTsimulationAlg")
808  << "Pulse polarity settings can be only +1.0 or -1.0 (got: "
809  << fBaseConfig.pulsePolarity << ")\n"
810  ;
811  } // check pulse polarity
812 
813  if (fBaseConfig.doGainFluctuations) {
814  double const mu0 = fBaseConfig.PMTspecs.firstStageGain();
815  if (!std::isnormal(mu0) || (mu0 < 0.0)) {
816  cet::exception e("PMTsimulationAlg");
817  e << "PMT gain " << fBaseConfig.PMTspecs.gain
818  << ", dynode resistance values {";
819  for (double rho: fBaseConfig.PMTspecs.voltageDistribution)
820  e << " " << rho;
821  e << " } and dynode k constant " << fBaseConfig.PMTspecs.dynodeK
822  << " resulted in an invalid gain " << mu0
823  << " for the first multiplication stage!\n";
824  throw e;
825  }
826  } // if invalid gain fluctuation
827 
828 } // icarus::opdet::PMTsimulationAlgMaker::PMTsimulationAlgMaker()
829 
830 
831 //-----------------------------------------------------------------------------
832 std::unique_ptr<icarus::opdet::PMTsimulationAlg>
836  SinglePhotonResponseFunc_t const& SPRfunction,
837  CLHEP::HepRandomEngine& mainRandomEngine,
838  CLHEP::HepRandomEngine& darkNoiseRandomEngine,
839  CLHEP::HepRandomEngine& elecNoiseRandomEngine,
840  bool trackSelectedPhotons /* = false */
841  ) const
842 {
843  return std::make_unique<PMTsimulationAlg>(makeParams(
844  larProp, clockData,
845  SPRfunction,
846  mainRandomEngine, darkNoiseRandomEngine, elecNoiseRandomEngine,
847  trackSelectedPhotons
848  ));
849 
850 } // icarus::opdet::PMTsimulationAlgMaker::operator()
851 
852 
853 //-----------------------------------------------------------------------------
857  SinglePhotonResponseFunc_t const& SPRfunction,
858  CLHEP::HepRandomEngine& mainRandomEngine,
859  CLHEP::HepRandomEngine& darkNoiseRandomEngine,
860  CLHEP::HepRandomEngine& elecNoiseRandomEngine,
861  bool trackSelectedPhotons /* = false */
863 {
864  using namespace util::quantities::electronics_literals;
865 
866  //
867  // set the configuration
868  //
869  auto params = fBaseConfig;
870 
871  //
872  // set up parameters
873  //
874  params.larProp = &larProp;
875  params.clockData = &clockData;
876 
877  params.pulseFunction = &SPRfunction;
878 
879  params.randomEngine = &mainRandomEngine;
880  params.gainRandomEngine = params.randomEngine;
881  params.darkNoiseRandomEngine = &darkNoiseRandomEngine;
882  params.elecNoiseRandomEngine = &elecNoiseRandomEngine;
883 
884  params.trackSelectedPhotons = trackSelectedPhotons;
885 
886  //
887  // setup checks
888  //
889  bool const expectedNegativePolarity
890  = (SPRfunction.peakAmplitude() < 0.0_ADCf);
891 
892  if (std::signbit(params.pulsePolarity) != expectedNegativePolarity) {
893  throw cet::exception("PMTsimulationAlg")
894  << "Inconsistent settings: pulse polarity declared "
895  << params.pulsePolarity << ", but photoelectron waveform amplitude is "
896  << SPRfunction.peakAmplitude()
897  << "\n"
898  ;
899  } // check polarity consistency
900 
901  return params;
902 
903 } // icarus::opdet::PMTsimulationAlgMaker::create()
904 
905 
906 //-----------------------------------------------------------------------------
double std(const std::vector< short > &wf, const double ped_mean, size_t start, size_t nsample)
Definition: UtilFunc.cxx:42
unsigned int pulseSubsamples
Number of tick subsamples.
size_t beamGateTriggerNReps
Number of beamgate trigger reps to produce.
unsigned int nDynodes() const
Number of dynodes in the PMTs.
double QEbase
Uncorrected PMT quantum efficiency.
PMTsimulationAlg::ConfigurationParameters_t makeParams(detinfo::LArProperties const &larProp, detinfo::DetectorClocksData const &clockData, SinglePhotonResponseFunc_t const &SPRfunction, CLHEP::HepRandomEngine &mainRandomEngine, CLHEP::HepRandomEngine &darkNoiseRandomEngine, CLHEP::HepRandomEngine &elecNoiseRandomEngine, bool trackSelectedPhotons=false) const
Returns a data structure to construct the algorithm.
static constexpr Sample_t transform(Sample_t sample)
int OpChannel() const
Returns the optical channel number this object is associated to.
Definition: SimPhotons.h:254
double dynodeK
Gain from stage with voltage dV is proportional to dV^K.
void AddPhotoelectrons(PulseSampling_t const &pulse, Waveform_t &wave, tick const time_bin, WaveformValue_t const n) const
Adds a number of pulses to a waveform, starting at a given tick.
microsecond_as<> microsecond
Type of time stored in microseconds, in double precision.
Definition: spacetime.h:119
Functor to convert tick point into a tick number and a subsample index.
fhicl::Atom< microseconds > TriggerOffsetPMT
ADCcount peakAmplitude() const
Returns the peak amplitude in ADC counts.
DiscretePhotoelectronPulse::Subsample_t PulseSampling_t
Type of sampled pulse shape: sequence of samples, one per tick.
DiscretePhotoelectronPulse::ADCcount ADCcount
bool useFastElectronicsNoise
Whether to use fast generator for electronics noise.
static constexpr quantity_t castFrom(U value)
Returns a new quantity initialized with the specified value.
Definition: quantities.h:825
bool createBeamGateTriggers
Option to create unbiased readout around beam spill.
std::tuple< std::vector< raw::OpDetWaveform >, std::optional< sim::SimPhotons > > simulate(sim::SimPhotons const &photons, sim::SimPhotonsLite const &lite_photons)
Returns the waveforms originating from simulated photons.
std::vector< raw::OpDetWaveform > CreateFixedSizeOpDetWaveforms(raw::Channel_t opChannel, Waveform_t const &waveform) const
Creates raw::OpDetWaveform objects from a waveform data.
constexpr value_t value() const
Returns the value of the quantity.
Definition: quantities.h:609
picocoulomb_as<> picocoulomb
Type of charge stored in picocoulomb, in double precision.
pdgs mu
Definition: selectors.fcl:22
DiscretePhotoelectronPulse wsp
CLHEP::HepRandomEngine * gainRandomEngine
Random stream engine for gain fluctuations.
Substitute for C++20 std::identity.
ADCcount thresholdADC
ADC Threshold for self-triggered readout.
void setVoltageDistribution(std::vector< double > &&Rs)
Sets voltageDistribution by stealing and normalizing Rs.
std::unique_ptr< PMTsimulationAlg > operator()(detinfo::LArProperties const &larProp, detinfo::DetectorClocksData const &detClocks, SinglePhotonResponseFunc_t const &SPRfunction, CLHEP::HepRandomEngine &mainRandomEngine, CLHEP::HepRandomEngine &darkNoiseRandomEngine, CLHEP::HepRandomEngine &elecNoiseRandomEngine, bool trackSelectedPhotons=false) const
Creates and returns a new algorithm instance.
auto enumerate(Iterables &&...iterables)
Range-for loop helper tracking the number of iteration.
Definition: enumerate.h:69
std::vector< optical_tick > FindTriggers(Waveform_t const &wvfm) const
Ticks in the specified waveform where some signal activity starts.
std::pair< ADCcount, ADCcount > saturationRange() const
Returns the ADC range allowed for photoelectron saturation.
std::map< int, int > DetectedPhotons
Number of photons detected at each given time: time tick -&gt; photons.
Definition: SimPhotons.h:117
Waveform_t CreateFullWaveform(sim::SimPhotons const &photons, sim::SimPhotonsLite const &lite_photons, std::optional< sim::SimPhotons > &photons_used) const
Creates raw::OpDetWaveform objects from simulated photoelectrons.
std::size_t fNsamples
Samples per waveform.
Interface to detinfo::DetectorClocks.
process_name gaushit a
OpDetWaveformMaker_t::WaveformData_t Waveform_t
Type internally used for storing waveforms.
tick_as< double > tick_d
Tick number, represented by double.
Definition: electronics.h:87
SinglePhotonResponseFunc_t const * pulseFunction
Single photon response function.
auto vector(Vector const &v)
Returns a manipulator which will print the specified array.
Definition: DumpUtils.h:265
timescale_traits< TriggerTimeCategory >::time_point_t trigger_time
A point in time on the trigger time scale.
ADCcount::value_t WaveformValue_t
Numeric type in waveforms.
T abs(T value)
PMTsimulationAlg(ConfigurationParameters_t const &config)
Constructor.
void ApplySaturation(Waveform_t &waveform, std::pair< ADCcount, ADCcount > const &range) const
std::vector< optical_tick > CreateBeamGateTriggers() const
Generate periodic interest points regardless the actual activity.
double multiplicationStageGain(unsigned int i=1) const
Returns the gain of the specified multiplication stage.
double distance(geo::Point_t const &point, CathodeDesc_t const &cathode)
Returns the distance of a point from the cathode.
fhicl::Atom< microseconds > ReadoutEnablePeriod
void AddDarkNoise(Waveform_t &wave) const
bool KicksPhotoelectron() const
Returns a random response whether a photon generates a photoelectron.
timescale_traits< SimulationTimeCategory >::time_point_t simulation_time
A point in time on the simulation time scale.
bool checkRange(ADCcount limit, std::string const &outputCat) const
Checks that the waveform tails not sampled are negligible.
CLHEP::HepRandomEngine * darkNoiseRandomEngine
Dark noise random stream engine.
A value measured in the specified unit.
Definition: quantities.h:566
detinfo::DetectorTimings makeDetectorTimings(detinfo::DetectorClocksData const &detClocks)
microseconds readoutEnablePeriod
Time (us) for which pmt readout is enabled.
util::quantities::hertz hertz
megahertz_as<> megahertz
Type of frequency stored in megahertz, in double precision.
Definition: frequency.h:101
Interface for a function describing a pulse from a photoelectron.
Test of util::counter and support utilities.
Operations on waveform samples.
PMTsimulationAlgMaker(Config const &config)
Constructor.
microseconds triggerOffsetPMT
Time relative to trigger when PMT readout starts TODO make it a trigger_time point.
An interval (duration, length, distance) between two quantity points.
Definition: intervals.h:114
fhicl::Atom< microseconds > BeamGateTriggerRepPeriod
detinfo::timescales::optical_tick optical_tick
ElecClock const & OpticalClock() const noexcept
Borrow a const Optical clock with time set to Trigger time [us].
ConfigurationParameters_t fParams
Complete algorithm configuration.
auto makeGainFluctuator() const
Returns a configured gain fluctuator object.
Pulse from one photoelectron as two half Gaussian functions.
virtual double ScintPreScale(bool prescale=true) const =0
CLHEP::HepRandomEngine * randomEngine
Main random stream engine.
Collection of photons which recorded on one channel.
Definition: SimPhotons.h:136
Compact representation of photons on a channel.
Definition: SimPhotons.h:103
void AddPulseShape(PulseSampling_t const &pulse, Waveform_t &wave, tick const time_bin, Combine combination) const
Adds a pulse to a waveform, starting at a given tick.
std::pair< ADCcount, ADCcount > ADCrange() const
Contains all timing reference information for the detector.
static void ClipWaveform(Waveform_t &waveform, ADCcount min, ADCcount max)
Forces waveform ADC within the min to max range (max included).
int pulsePolarity
Pulse polarity (=1 for positive, =-1 for negative)
megahertz fSampling
Wave sampling frequency [MHz].
double fQE
PMT quantum efficiency.
then echo File list $list not found else cat $list while read file do echo $file sed s
Definition: file_to_url.sh:60
NoiseAdderFunc_t const fNoiseAdder
Single photon pulse (sampled).
A class exposing an upgraded interface of detinfo::DetectorClocksData.
process_name largeant stream1 can override from command line with o or output physics producers generator N
constexpr T && operator()(T &&v) const noexcept
Functions pulling in STL customization if available.
nanosecond_as<> nanosecond
Type of time stored in nanoseconds, in double precision.
Definition: spacetime.h:136
do i e
Type holding all configuration parameters for this algorithm.
Algorithms for the simulation of ICARUS PMT channels.
constexpr double Frequency() const
Frequency in MHz.
Definition: ElecClock.h:191
detinfo::LArProperties const * larProp
LarProperties service provider.
fhicl::Atom< std::size_t > BeamGateTriggerNReps
bool trackSelectedPhotons
Whether to track the scintillation photons used.
void AddNoise(Waveform_t &wave) const
DiscretePhotoelectronPulse::SubsampleIndex_t SubsampleIndex_t
pdgs k
Definition: selectors.fcl:22
decltype(auto) subsample(SubsampleIndex_t i) const
Returns the subsample specified by index i, undefined if invalid.
microseconds beamGateTriggerRepPeriod
Repetition Period (us) for BeamGateTriggers TODO make this a time_interval.
std::pair< detinfo::timescales::optical_tick, detinfo::timescales::optical_tick > BufferRange_t
CLHEP::HepRandomEngine * elecNoiseRandomEngine
Electronics noise random stream engine.
double firstStageGain() const
Returns the gain from the first stage of PMT multiplication.
void AddNoise_faster(Waveform_t &wave) const
Same as AddNoise() but using an alternative generator.
hertz_as<> hertz
Type of frequency stored in hertz, in double precision.
Definition: frequency.h:81
Helper class to cut a raw::OpDetWaveform from a longer waveform data.
static util::FastAndPoorGauss< 32768U, float > const fFastGauss
Main algorithm FHiCL configuration.