Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-03-31 07:48:20

0001 // SPDX-License-Identifier: LGPL-3.0-or-later
0002 // Copyright (C) 2022 - 2025 Chao Peng, Wouter Deconinck, Sylvester Joosten, Barak Schmookler, David Lawrence, Akio Ogawa
0003 
0004 // A general digitization for CalorimeterHit from simulation
0005 // 1. Smear energy deposit with a/sqrt(E/GeV) + b + c/E or a/sqrt(E/GeV) (relative value)
0006 // 2. Digitize the energy with dynamic ADC range and add pedestal (mean +- sigma)
0007 // 3. Time conversion with smearing resolution (absolute value)
0008 // 4. Signal is summed if the SumFields are provided
0009 //
0010 // Author: Chao Peng
0011 // Date: 06/02/2021
0012 
0013 #include "CalorimeterHitDigi.h"
0014 
0015 #include <DD4hep/Detector.h>
0016 #include <DD4hep/IDDescriptor.h>
0017 #include <DD4hep/Readout.h>
0018 #include <DDSegmentation/BitFieldCoder.h>
0019 #include <Evaluator/DD4hepUnits.h>
0020 #include <algorithms/service.h>
0021 #include <edm4eic/EDM4eicVersion.h>
0022 #include <edm4eic/MCRecoCalorimeterHitAssociationCollection.h>
0023 #include <edm4hep/CaloHitContributionCollection.h>
0024 #include <fmt/format.h>
0025 #include <podio/RelationRange.h>
0026 #include <podio/detail/Link.h>
0027 #include <podio/detail/LinkCollectionImpl.h>
0028 #include <algorithm>
0029 #include <cmath>
0030 #include <cstddef>
0031 #include <gsl/pointers>
0032 #include <limits>
0033 #include <map>
0034 #include <memory>
0035 #include <random>
0036 #include <stdexcept>
0037 #include <string>
0038 #include <unordered_map>
0039 #include <utility>
0040 #include <vector>
0041 
0042 #include "algorithms/calorimetry/CalorimeterHitDigiConfig.h"
0043 #include "services/evaluator/EvaluatorSvc.h"
0044 
0045 using namespace dd4hep;
0046 
0047 namespace eicrecon {
0048 
0049 //
0050 // TODO:
0051 // - Array type configuration parameters are not yet supported in JANA (needs to be added)
0052 // - Random number service needs to bew resolved (on global scale)
0053 // - It is possible standard running of this with Gaudi relied on a number of parameters
0054 //   being set in the config. If that is the case, they should be moved into the default
0055 //   values here. This needs to be confirmed.
0056 
0057 void CalorimeterHitDigi::init() {
0058 
0059   // Gaudi implements a random number generator service. It is not clear to me how this
0060   // can work. There are multiple race conditions that occur in parallel event processing:
0061   // 1. The exact same events processed by a given thread in one invocation will not
0062   //    necessarily be the combination of events any thread sees in a subsequent
0063   //    invocation. Thus, you can't rely on thread_local storage.
0064   // 2. Its possible for the factory execution order to be modified by the presence of
0065   //    a processor (e.g. monitoring plugin). This is not as serious since changing the
0066   //    command line should cause one not to expect reproducibility. Still, one may
0067   //    expect the inclusion of an "observer" plugin not to have such side affects.
0068   //
0069   // More information will be needed. In the meantime, we implement a local random number
0070   // generator. Ideally, this would be seeded with the run number+event number, but for
0071   // now, just use default values defined in header file.
0072 
0073   // set energy resolution numbers
0074   if (m_cfg.eRes.empty()) {
0075     m_cfg.eRes.resize(3);
0076   } else if (m_cfg.eRes.size() != 3) {
0077     error("Invalid m_cfg.eRes.size()");
0078     throw std::runtime_error("Invalid m_cfg.eRes.size()");
0079   }
0080 
0081   // using juggler internal units (GeV, mm, radian, ns)
0082   tRes    = m_cfg.tRes / dd4hep::ns;
0083   stepTDC = dd4hep::ns / m_cfg.resolutionTDC;
0084 
0085   // sanity checks
0086   if (m_cfg.readout.empty()) {
0087     error("readoutClass is not provided, it is needed to know the fields in readout ids");
0088     throw std::runtime_error("readoutClass is not provided");
0089   }
0090 
0091   // get decoders
0092   try {
0093     id_spec = m_geo.detector()->readout(m_cfg.readout).idSpec();
0094   } catch (...) {
0095     // Can not be more verbose. In JANA2, this will be attempted at each event, which
0096     // pollutes output for geometries that are less than complete.
0097     // We could save an exception and throw it from process.
0098     debug("Failed to load ID decoder for {}", m_cfg.readout);
0099     throw std::runtime_error(fmt::format("Failed to load ID decoder for {}", m_cfg.readout));
0100   }
0101 
0102   decltype(id_mask) id_inverse_mask = 0;
0103   // all these are for signal sum at digitization level
0104   if (!m_cfg.fields.empty()) {
0105     for (auto& field : m_cfg.fields) {
0106       id_inverse_mask |= id_spec.field(field)->mask();
0107     }
0108     debug("ID mask in {:s}: {:#064b}", m_cfg.readout, id_mask);
0109   }
0110   id_mask = ~id_inverse_mask;
0111 
0112   std::function hit_to_map = [this](const edm4hep::SimCalorimeterHit& h) {
0113     std::unordered_map<std::string, double> params;
0114     for (const auto& p : id_spec.fields()) {
0115       const std::string& name                  = p.first;
0116       const dd4hep::IDDescriptor::Field* field = p.second;
0117       params.emplace(name, field->value(h.getCellID()));
0118       trace("{} = {}", name, field->value(h.getCellID()));
0119     }
0120     return params;
0121   };
0122 
0123   auto& serviceSvc = algorithms::ServiceSvc::instance();
0124   corrMeanScale =
0125       serviceSvc.service<EvaluatorSvc>("EvaluatorSvc")->compile(m_cfg.corrMeanScale, hit_to_map);
0126 
0127   std::map<std::string, readout_enum> readoutTypes{{"simple", kSimpleReadout},
0128                                                    {"poisson_photon", kPoissonPhotonReadout},
0129                                                    {"sipm", kSipmReadout}};
0130   if (not readoutTypes.count(m_cfg.readoutType)) {
0131     error("Invalid readoutType \"{}\"", m_cfg.readoutType);
0132     throw std::runtime_error(fmt::format("Invalid readoutType \"{}\"", m_cfg.readoutType));
0133   }
0134   readoutType = readoutTypes.at(m_cfg.readoutType);
0135 }
0136 
0137 void CalorimeterHitDigi::process(const CalorimeterHitDigi::Input& input,
0138                                  const CalorimeterHitDigi::Output& output) const {
0139 
0140   const auto [headers, simhits] = input;
0141 #if EDM4EIC_BUILD_VERSION >= EDM4EIC_VERSION(8, 7, 0)
0142   auto [rawhits, links, rawassocs] = output;
0143 #else
0144   auto [rawhits, rawassocs] = output;
0145 #endif
0146 
0147   // local random generator
0148   auto seed = m_uid.getUniqueID(*headers, name());
0149   std::default_random_engine generator(seed);
0150   std::normal_distribution<double> gaussian;
0151 
0152   // find the hits that belong to the same group (for merging)
0153   std::unordered_map<uint64_t, std::vector<std::size_t>> merge_map;
0154   std::size_t ix = 0;
0155   for (const auto& ahit : *simhits) {
0156     uint64_t hid = ahit.getCellID() & id_mask;
0157 
0158     trace("org cell ID in {:s}: {:#064b}", m_cfg.readout, ahit.getCellID());
0159     trace("new cell ID in {:s}: {:#064b}", m_cfg.readout, hid);
0160 
0161     merge_map[hid].push_back(ix);
0162 
0163     ix++;
0164   }
0165 
0166   // signal sum
0167   // NOTE: we take the cellID of the most energetic hit in this group so it is a real cellID from an MC hit
0168   for (const auto& [id, ixs] : merge_map) {
0169 
0170     // create hit and association in advance
0171     edm4hep::MutableRawCalorimeterHit rawhit;
0172 #if EDM4EIC_BUILD_VERSION >= EDM4EIC_VERSION(8, 7, 0)
0173     std::vector<edm4eic::MutableMCRecoCalorimeterHitLink> links_staging;
0174 #endif
0175     std::vector<edm4eic::MutableMCRecoCalorimeterHitAssociation> rawassocs_staging;
0176 
0177     double edep      = 0;
0178     double time      = std::numeric_limits<double>::max();
0179     double max_edep  = 0;
0180     auto leading_hit = (*simhits)[ixs[0]];
0181     // sum energy, take time from the most energetic hit
0182     for (unsigned long i : ixs) {
0183       auto hit = (*simhits)[i];
0184 
0185       double timeC = std::numeric_limits<double>::max();
0186       for (const auto& c : hit.getContributions()) {
0187         timeC = std::min<double>(c.getTime(), timeC);
0188       }
0189       if (timeC > m_cfg.capTime) {
0190         debug("retaining hit, even though time %f ns > %f ns", timeC / dd4hep::ns,
0191               m_cfg.capTime / dd4hep::ns);
0192       }
0193       edep += hit.getEnergy();
0194       trace("adding {} \t total: {}", hit.getEnergy(), edep);
0195 
0196       // change maximum hit energy & time if necessary
0197       if (hit.getEnergy() > max_edep) {
0198         max_edep    = hit.getEnergy();
0199         leading_hit = hit;
0200         time        = std::min(timeC, time);
0201       }
0202 
0203       edm4eic::MutableMCRecoCalorimeterHitAssociation assoc;
0204       assoc.setRawHit(rawhit);
0205       assoc.setSimHit(hit);
0206       assoc.setWeight(hit.getEnergy());
0207       rawassocs_staging.push_back(assoc);
0208 #if EDM4EIC_BUILD_VERSION >= EDM4EIC_VERSION(8, 7, 0)
0209       edm4eic::MutableMCRecoCalorimeterHitLink link;
0210       link.setFrom(rawhit);
0211       link.setTo(hit);
0212       link.setWeight(hit.getEnergy());
0213       links_staging.push_back(link);
0214 #endif
0215     }
0216     if (time > m_cfg.capTime) {
0217       debug("retaining hit, even though time %f ns > %f ns", time / dd4hep::ns,
0218             m_cfg.capTime / dd4hep::ns);
0219     }
0220 
0221     // safety check
0222     const double eResRel =
0223         (edep > m_cfg.threshold)
0224             ? gaussian(generator) *
0225                   std::sqrt(std::pow(m_cfg.eRes[0] / std::sqrt(edep), 2) +
0226                             std::pow(m_cfg.eRes[1], 2) + std::pow(m_cfg.eRes[2] / (edep), 2))
0227             : 0;
0228 
0229     double corrMeanScale_value = corrMeanScale(leading_hit);
0230 
0231     double ped = m_cfg.pedMeanADC + gaussian(generator) * m_cfg.pedSigmaADC;
0232 
0233     // Note: both adc and tdc values must be positive numbers to avoid integer wraparound
0234     unsigned long long adc = 0;
0235     unsigned long long tdc = std::llround((time + gaussian(generator) * tRes) * stepTDC);
0236 
0237     //smear edep by resolution function before photon and SiPM simulation
0238     edep *= std::max(0.0, 1.0 + eResRel);
0239 
0240     if (readoutType == kSimpleReadout) {
0241       adc = std::max(
0242           std::llround(ped + edep * corrMeanScale_value / m_cfg.dyRangeADC * m_cfg.capADC), 0LL);
0243     } else if (readoutType == kPoissonPhotonReadout) {
0244       const long long int n_photons_mean =
0245           edep * m_cfg.lightYield * m_cfg.photonDetectionEfficiency;
0246       std::poisson_distribution<> n_photons_detected_dist(n_photons_mean);
0247       const long long int n_photons_detected = n_photons_detected_dist(generator);
0248       const long long int n_max_photons =
0249           m_cfg.dyRangeADC * m_cfg.lightYield * m_cfg.photonDetectionEfficiency;
0250       trace("n_photons_detected {}", n_photons_detected);
0251       adc = std::max(std::llround(ped + n_photons_detected * corrMeanScale_value / n_max_photons *
0252                                             m_cfg.capADC),
0253                      0LL);
0254     } else if (readoutType == kSipmReadout) {
0255       const long long int n_photons = edep * m_cfg.lightYield;
0256       std::binomial_distribution<> n_photons_detected_dist(n_photons,
0257                                                            m_cfg.photonDetectionEfficiency);
0258       const long long int n_photons_detected = n_photons_detected_dist(generator);
0259       const long long int n_pixels_fired =
0260           m_cfg.numEffectiveSipmPixels *
0261           (1 - exp(-n_photons_detected / (double)m_cfg.numEffectiveSipmPixels));
0262       const long long int n_max_photons =
0263           m_cfg.dyRangeADC * m_cfg.lightYield * m_cfg.photonDetectionEfficiency;
0264       trace("n_photons_detected {}, n_pixels_fired {}, n_max_photons {}", n_photons_detected,
0265             n_pixels_fired, n_max_photons);
0266       adc = std::max(
0267           std::llround(ped + n_pixels_fired * corrMeanScale_value / n_max_photons * m_cfg.capADC),
0268           0LL);
0269     }
0270 
0271     if (edep > 1.e-3) {
0272       trace("E sim {} \t adc: {} \t time: {}\t maxtime: {} \t tdc: {} \t corrMeanScale: {}", edep,
0273             adc, time, m_cfg.capTime, tdc, corrMeanScale_value);
0274     }
0275 
0276     rawhit.setCellID(leading_hit.getCellID());
0277     rawhit.setAmplitude(adc > m_cfg.capADC ? m_cfg.capADC : adc);
0278     rawhit.setTimeStamp(tdc);
0279     rawhits->push_back(rawhit);
0280 
0281 #if EDM4EIC_BUILD_VERSION >= EDM4EIC_VERSION(8, 7, 0)
0282     for (auto& link : links_staging) {
0283       link.setWeight(link.getWeight() / edep);
0284       links->push_back(link);
0285     }
0286 #endif
0287     for (auto& assoc : rawassocs_staging) {
0288       assoc.setWeight(assoc.getWeight() / edep);
0289       rawassocs->push_back(assoc);
0290     }
0291   }
0292 }
0293 
0294 } // namespace eicrecon