File indexing completed on 2025-07-14 08:14:36
0001
0002
0003
0004 #include "IrtCherenkovParticleID.h"
0005
0006 #include <IRT/ChargedParticle.h>
0007 #include <IRT/CherenkovPID.h>
0008 #include <IRT/OpticalPhoton.h>
0009 #include <IRT/RadiatorHistory.h>
0010 #include <IRT/SinglePDF.h>
0011 #include <TString.h>
0012 #include <TVector3.h>
0013 #include <algorithms/logger.h>
0014 #include <edm4eic/CherenkovParticleIDHypothesis.h>
0015 #include <edm4eic/TrackPoint.h>
0016 #include <edm4hep/EDM4hepVersion.h>
0017 #include <edm4hep/MCParticleCollection.h>
0018 #include <edm4hep/SimTrackerHitCollection.h>
0019 #include <edm4hep/Vector2f.h>
0020 #include <fmt/core.h>
0021 #include <fmt/format.h>
0022 #include <cmath>
0023
0024 #include <algorithm>
0025 #include <cstddef>
0026 #include <functional>
0027 #include <gsl/pointers>
0028 #include <iterator>
0029 #include <memory>
0030 #include <podio/ObjectID.h>
0031 #include <podio/RelationRange.h>
0032 #include <set>
0033 #include <stdexcept>
0034 #include <utility>
0035 #include <vector>
0036
0037 #include "algorithms/pid/IrtCherenkovParticleIDConfig.h"
0038 #include "algorithms/pid/Tools.h"
0039
0040 namespace eicrecon {
0041
0042 void IrtCherenkovParticleID::init(CherenkovDetectorCollection* irt_det_coll) {
0043
0044 m_irt_det_coll = irt_det_coll;
0045
0046
0047 debug() << m_cfg << endmsg;
0048
0049
0050 auto print_cheat = [this](auto name, bool val, auto desc) {
0051 if (val) {
0052 warning("CHEAT MODE '{}' ENABLED: {}", name, desc);
0053 }
0054 };
0055 print_cheat("cheatPhotonVertex", m_cfg.cheatPhotonVertex,
0056 "use MC photon vertex, wavelength, refractive index");
0057 print_cheat("cheatTrueRadiator", m_cfg.cheatTrueRadiator, "use MC truth to obtain true radiator");
0058
0059
0060 const auto& detectors = m_irt_det_coll->GetDetectors();
0061 if (detectors.empty()) {
0062 throw std::runtime_error("No CherenkovDetectors found in input collection `irt_det_coll`");
0063 }
0064 if (detectors.size() > 1) {
0065 warning("IrtCherenkovParticleID currently only supports 1 CherenkovDetector at a time; "
0066 "taking the first");
0067 }
0068 auto this_detector = *detectors.begin();
0069 m_det_name = this_detector.first;
0070 m_irt_det = this_detector.second;
0071 debug("Initializing IrtCherenkovParticleID algorithm for CherenkovDetector '{}'", m_det_name);
0072
0073
0074 m_cell_mask = m_irt_det->GetReadoutCellMask();
0075 debug("readout cellMask = {:#X}", m_cell_mask);
0076
0077
0078 trace("Rebinning refractive index tables to have {} bins", m_cfg.numRIndexBins);
0079 for (auto [rad_name, irt_rad] : m_irt_det->Radiators()) {
0080
0081 std::lock_guard<std::mutex> lock(m_irt_det_mutex);
0082 auto ri_lookup_table_orig = irt_rad->m_ri_lookup_table;
0083 if (ri_lookup_table_orig.size() != m_cfg.numRIndexBins) {
0084 irt_rad->m_ri_lookup_table.clear();
0085 irt_rad->m_ri_lookup_table =
0086 Tools::ApplyFineBinning(ri_lookup_table_orig, m_cfg.numRIndexBins);
0087 }
0088 }
0089
0090
0091 debug("Obtain List of Radiators:");
0092 for (auto [rad_name, irt_rad] : m_irt_det->Radiators()) {
0093 if (rad_name != "Filter") {
0094 m_pid_radiators.insert({std::string(rad_name), irt_rad});
0095 debug("- {}", rad_name.Data());
0096 }
0097 }
0098
0099
0100 for (auto [rad_name, irt_rad] : m_pid_radiators) {
0101 std::lock_guard<std::mutex> lock(m_irt_det_mutex);
0102
0103 auto cfg_rad_it = m_cfg.radiators.find(rad_name);
0104 if (cfg_rad_it != m_cfg.radiators.end()) {
0105 auto cfg_rad = cfg_rad_it->second;
0106
0107 irt_rad->m_ID = Tools::GetRadiatorID(std::string(rad_name));
0108 irt_rad->m_AverageRefractiveIndex = cfg_rad.referenceRIndex;
0109 irt_rad->SetReferenceRefractiveIndex(cfg_rad.referenceRIndex);
0110 if (cfg_rad.attenuation > 0) {
0111 irt_rad->SetReferenceAttenuationLength(cfg_rad.attenuation);
0112 }
0113 if (cfg_rad.smearing > 0) {
0114 if (cfg_rad.smearingMode == "uniform") {
0115 irt_rad->SetUniformSmearing(cfg_rad.smearing);
0116 } else if (cfg_rad.smearingMode == "gaussian") {
0117 irt_rad->SetGaussianSmearing(cfg_rad.smearing);
0118 } else {
0119 error("Unknown smearing mode '{}' for {} radiator", cfg_rad.smearingMode, rad_name);
0120 }
0121 }
0122 } else {
0123 error("Cannot find radiator '{}' in IrtCherenkovParticleIDConfig instance", rad_name);
0124 }
0125 }
0126
0127
0128 debug("List of particles for PID:");
0129 for (auto pdg : m_cfg.pdgList) {
0130 auto mass = m_particleSvc.particle(pdg).mass;
0131 m_pdg_mass.insert({pdg, mass});
0132 debug(" {:>8} M={} GeV", pdg, mass);
0133 }
0134 }
0135
0136 void IrtCherenkovParticleID::process(const IrtCherenkovParticleID::Input& input,
0137 const IrtCherenkovParticleID::Output& output) const {
0138 const auto [in_aerogel_tracks, in_gas_tracks, in_merged_tracks, in_raw_hits, in_hit_assocs] =
0139 input;
0140 auto [out_aerogel_particleIDs, out_gas_particleIDs] = output;
0141
0142
0143 trace("{:=^70}", " call IrtCherenkovParticleID::AlgorithmProcess ");
0144 trace("number of raw sensor hits: {}", in_raw_hits->size());
0145 trace("number of raw sensor hit with associated photons: {}", in_hit_assocs->size());
0146
0147 std::map<std::string, const edm4eic::TrackSegmentCollection*> in_charged_particles{
0148 {"Aerogel", in_aerogel_tracks},
0149 {"Gas", in_gas_tracks},
0150 {"Merged", in_merged_tracks},
0151 };
0152
0153
0154 std::map<std::string, edm4eic::CherenkovParticleIDCollection*> out_cherenkov_pids{
0155 {"Aerogel", out_aerogel_particleIDs}, {"Gas", out_gas_particleIDs}};
0156
0157
0158 std::unordered_map<std::size_t, std::size_t> in_charged_particle_size_distribution;
0159 for (const auto& [rad_name, in_charged_particle] : in_charged_particles) {
0160 ++in_charged_particle_size_distribution[in_charged_particle->size()];
0161 }
0162 if (in_charged_particle_size_distribution.size() != 1) {
0163 std::vector<std::size_t> in_charged_particle_sizes;
0164 std::transform(
0165 in_charged_particles.begin(), in_charged_particles.end(),
0166 std::back_inserter(in_charged_particle_sizes),
0167 [](const auto& in_charged_particle) { return in_charged_particle.second->size(); });
0168 error("radiators have differing numbers of TrackSegments {}",
0169 fmt::join(in_charged_particle_sizes, ", "));
0170 return;
0171 }
0172
0173
0174 trace("{:#<70}", "### CHARGED PARTICLES ");
0175 std::size_t num_charged_particles = in_charged_particle_size_distribution.begin()->first;
0176 for (std::size_t i_charged_particle = 0; i_charged_particle < num_charged_particles;
0177 i_charged_particle++) {
0178 trace("{:-<70}", fmt::format("--- charged particle #{} ", i_charged_particle));
0179
0180
0181 auto irt_particle = std::make_unique<ChargedParticle>();
0182
0183
0184
0185
0186 std::lock_guard<std::mutex> lock(m_irt_det_mutex);
0187 for (auto [rad_name, irt_rad] : m_pid_radiators) {
0188
0189
0190 auto charged_particle_list_it = in_charged_particles.find(rad_name);
0191 if (charged_particle_list_it == in_charged_particles.end()) {
0192 error("Cannot find radiator '{}' in `in_charged_particles`", rad_name);
0193 continue;
0194 }
0195 const auto* charged_particle_list = charged_particle_list_it->second;
0196 auto charged_particle = charged_particle_list->at(i_charged_particle);
0197
0198
0199 if (charged_particle.points_size() == 0) {
0200 trace("No propagated track points in radiator '{}'", rad_name);
0201 continue;
0202 }
0203 irt_rad->SetTrajectoryBinCount(charged_particle.points_size() - 1);
0204
0205
0206
0207
0208 auto* irt_rad_history = new RadiatorHistory();
0209 irt_particle->StartRadiatorHistory({irt_rad, irt_rad_history});
0210
0211
0212 irt_rad->ResetLocations();
0213 trace("TrackPoints in '{}' radiator:", rad_name);
0214 for (const auto& point : charged_particle.getPoints()) {
0215 TVector3 position = Tools::PodioVector3_to_TVector3(point.position);
0216 TVector3 momentum = Tools::PodioVector3_to_TVector3(point.momentum);
0217 irt_rad->AddLocation(position, momentum);
0218 trace(Tools::PrintTVector3(" point: x", position));
0219 trace(Tools::PrintTVector3(" p", momentum));
0220 }
0221
0222
0223 trace("{:#<70}", "### SENSOR HITS ");
0224 for (const auto& raw_hit : *in_raw_hits) {
0225
0226
0227
0228
0229 edm4hep::MCParticle mc_photon;
0230 bool mc_photon_found = false;
0231 if (m_cfg.cheatPhotonVertex || m_cfg.cheatTrueRadiator) {
0232 for (const auto& hit_assoc : *in_hit_assocs) {
0233 if (hit_assoc.getRawHit().isAvailable()) {
0234 if (hit_assoc.getRawHit().id() == raw_hit.id()) {
0235 #if EDM4HEP_BUILD_VERSION >= EDM4HEP_VERSION(0, 99, 0)
0236 mc_photon = hit_assoc.getSimHit().getParticle();
0237 #else
0238 mc_photon = hit_assoc.getSimHit().getMCParticle();
0239 #endif
0240 mc_photon_found = true;
0241 if (mc_photon.getPDG() != -22) {
0242 warning("non-opticalphoton hit: PDG = {}", mc_photon.getPDG());
0243 }
0244 break;
0245 }
0246 }
0247 }
0248 }
0249
0250
0251 if (m_cfg.cheatTrueRadiator && mc_photon_found) {
0252 auto vtx = Tools::PodioVector3_to_TVector3(mc_photon.getVertex());
0253 auto* mc_rad = m_irt_det->GuessRadiator(vtx, vtx);
0254 if (mc_rad != irt_rad) {
0255 continue;
0256 }
0257 trace(Tools::PrintTVector3(
0258 fmt::format("cheat: radiator '{}' determined from photon vertex", rad_name), vtx));
0259 }
0260
0261
0262
0263 auto cell_id = raw_hit.getCellID();
0264 uint64_t sensor_id = cell_id & m_cell_mask;
0265 TVector3 pixel_pos = m_irt_det->m_ReadoutIDToPosition(cell_id);
0266
0267
0268 if (level() <= algorithms::LogLevel::kTrace) {
0269 trace("cell_id={:#X} sensor_id={:#X}", cell_id, sensor_id);
0270 trace(Tools::PrintTVector3("pixel position", pixel_pos));
0271 if (mc_photon_found) {
0272 TVector3 mc_endpoint = Tools::PodioVector3_to_TVector3(mc_photon.getEndpoint());
0273 trace(Tools::PrintTVector3("photon endpoint", mc_endpoint));
0274 trace("{:>30} = {}", "dist( pixel, photon )", (pixel_pos - mc_endpoint).Mag());
0275 } else {
0276 trace(" no MC photon found; probably a noise hit");
0277 }
0278 }
0279
0280
0281 auto* irt_sensor = m_irt_det->m_PhotonDetectors[0];
0282 auto* irt_photon =
0283 new OpticalPhoton();
0284 irt_photon->SetVolumeCopy(sensor_id);
0285 irt_photon->SetDetectionPosition(pixel_pos);
0286 irt_photon->SetPhotonDetector(irt_sensor);
0287 irt_photon->SetDetected(true);
0288
0289
0290 if ((m_cfg.cheatPhotonVertex || m_cfg.cheatTrueRadiator) && mc_photon_found) {
0291 irt_photon->SetVertexPosition(Tools::PodioVector3_to_TVector3(mc_photon.getVertex()));
0292 irt_photon->SetVertexMomentum(Tools::PodioVector3_to_TVector3(mc_photon.getMomentum()));
0293 }
0294
0295
0296
0297 if (m_cfg.cheatPhotonVertex) {
0298 double ri = NAN;
0299 auto mom = 1e9 * irt_photon->GetVertexMomentum().Mag();
0300 auto ri_set = Tools::GetFinelyBinnedTableEntry(irt_rad->m_ri_lookup_table, mom, &ri);
0301 if (ri_set) {
0302 irt_photon->SetVertexRefractiveIndex(ri);
0303 trace("{:>30} = {}", "refractive index", ri);
0304 } else {
0305 warning("Tools::GetFinelyBinnedTableEntry failed to lookup refractive index for "
0306 "momentum {} eV",
0307 mom);
0308 }
0309 }
0310
0311
0312
0313
0314
0315 irt_rad_history->AddOpticalPhoton(irt_photon);
0316
0317
0318
0319
0320
0321 }
0322
0323 }
0324
0325
0326
0327
0328 trace("{:+^70}", " PARTICLE IDENTIFICATION ");
0329 CherenkovPID irt_pid;
0330 std::unordered_map<int, MassHypothesis*> pdg_to_hyp;
0331 for (auto [pdg, mass] : m_pdg_mass) {
0332 irt_pid.AddMassHypothesis(mass);
0333 pdg_to_hyp.insert({pdg, irt_pid.GetHypothesis(irt_pid.GetHypothesesCount() - 1)});
0334 }
0335
0336
0337 irt_particle->PIDReconstruction(irt_pid);
0338 trace("{:-^70}", " IRT RESULTS ");
0339
0340
0341 for (auto [rad_name, irt_rad] : m_pid_radiators) {
0342 trace("-> {} Radiator (ID={}):", rad_name, irt_rad->m_ID);
0343
0344
0345 unsigned npe = 0;
0346 double rindex_ave = 0.0;
0347 double energy_ave = 0.0;
0348 std::vector<std::pair<double, double>> phot_theta_phi;
0349
0350
0351 auto* irt_rad_history = irt_particle->FindRadiatorHistory(irt_rad);
0352 if (irt_rad_history == nullptr) {
0353 trace(" No radiator history; skip");
0354 continue;
0355 }
0356 trace(" Photoelectrons:");
0357 for (auto* irt_photon : irt_rad_history->Photons()) {
0358
0359
0360 bool photon_selected = false;
0361 for (auto irt_photon_sel : irt_photon->_m_Selected) {
0362 if (irt_photon_sel.second == irt_rad) {
0363 photon_selected = true;
0364 break;
0365 }
0366 }
0367 if (!photon_selected) {
0368 continue;
0369 }
0370
0371
0372 trace(
0373 Tools::PrintTVector3(fmt::format("- sensor_id={:#X}: hit", irt_photon->GetVolumeCopy()),
0374 irt_photon->GetDetectionPosition()));
0375 trace(Tools::PrintTVector3("photon vertex", irt_photon->GetVertexPosition()));
0376
0377
0378 auto phot_theta = irt_photon->_m_PDF[irt_rad].GetAverage();
0379 auto phot_phi = irt_photon->m_Phi[irt_rad];
0380
0381
0382 npe++;
0383 phot_theta_phi.emplace_back(phot_theta, phot_phi);
0384 if (m_cfg.cheatPhotonVertex) {
0385 rindex_ave += irt_photon->GetVertexRefractiveIndex();
0386 energy_ave += irt_photon->GetVertexMomentum().Mag();
0387 }
0388
0389 }
0390
0391
0392 if (npe > 0) {
0393 rindex_ave /= npe;
0394 energy_ave /= npe;
0395 }
0396
0397
0398 auto out_cherenkov_pid = out_cherenkov_pids.at(rad_name)->create();
0399 out_cherenkov_pid.setNpe(static_cast<decltype(edm4eic::CherenkovParticleIDData::npe)>(npe));
0400 out_cherenkov_pid.setRefractiveIndex(
0401 static_cast<decltype(edm4eic::CherenkovParticleIDData::refractiveIndex)>(rindex_ave));
0402 out_cherenkov_pid.setPhotonEnergy(
0403 static_cast<decltype(edm4eic::CherenkovParticleIDData::photonEnergy)>(energy_ave));
0404 for (auto [phot_theta, phot_phi] : phot_theta_phi) {
0405 out_cherenkov_pid.addToThetaPhiPhotons(
0406 edm4hep::Vector2f{static_cast<float>(phot_theta), static_cast<float>(phot_phi)});
0407 }
0408
0409
0410 for (auto [pdg, mass] : m_pdg_mass) {
0411
0412
0413 auto* irt_hypothesis = pdg_to_hyp.at(pdg);
0414 auto hyp_weight = irt_hypothesis->GetWeight(irt_rad);
0415 auto hyp_npe = irt_hypothesis->GetNpe(irt_rad);
0416
0417
0418 edm4eic::CherenkovParticleIDHypothesis out_hypothesis;
0419 out_hypothesis.PDG =
0420 static_cast<decltype(edm4eic::CherenkovParticleIDHypothesis::PDG)>(pdg);
0421 out_hypothesis.weight =
0422 static_cast<decltype(edm4eic::CherenkovParticleIDHypothesis::weight)>(hyp_weight);
0423 out_hypothesis.npe =
0424 static_cast<decltype(edm4eic::CherenkovParticleIDHypothesis::npe)>(hyp_npe);
0425
0426
0427 out_cherenkov_pid.addToHypotheses(out_hypothesis);
0428
0429 }
0430
0431
0432 auto PrintCherenkovEstimate = [this](edm4eic::CherenkovParticleID pid,
0433 bool printHypotheses = true, int indent = 2) {
0434 double thetaAve = 0;
0435 if (pid.getNpe() > 0) {
0436 for (const auto& [theta, phi] : pid.getThetaPhiPhotons()) {
0437 thetaAve += theta / pid.getNpe();
0438 }
0439 }
0440 trace("{:{}}Cherenkov Angle Estimate:", "", indent);
0441 trace("{:{}} {:>16}: {:>10}", "", indent, "NPE", pid.getNpe());
0442 trace("{:{}} {:>16}: {:>10.8} mrad", "", indent, "<theta>",
0443 thetaAve * 1e3);
0444 trace("{:{}} {:>16}: {:>10.8}", "", indent, "<rindex>", pid.getRefractiveIndex());
0445 trace("{:{}} {:>16}: {:>10.8} eV", "", indent, "<energy>",
0446 pid.getPhotonEnergy() * 1e9);
0447 if (printHypotheses) {
0448 trace("{:{}}Mass Hypotheses:", "", indent);
0449 trace("{}", Tools::HypothesisTableHead(indent + 2));
0450 for (const auto& hyp : pid.getHypotheses()) {
0451 trace("{}", Tools::HypothesisTableLine(hyp, indent + 2));
0452 }
0453 }
0454 };
0455 PrintCherenkovEstimate(out_cherenkov_pid);
0456
0457
0458 auto charged_particle_list_it = in_charged_particles.find("Merged");
0459 if (charged_particle_list_it != in_charged_particles.end()) {
0460 const auto* charged_particle_list = charged_particle_list_it->second;
0461 auto charged_particle = charged_particle_list->at(i_charged_particle);
0462 out_cherenkov_pid.setChargedParticle(charged_particle);
0463 } else {
0464 error("Cannot find radiator 'Merged' in `in_charged_particles`");
0465 }
0466
0467
0468 for (const auto& hit_assoc : *in_hit_assocs) {
0469 out_cherenkov_pid.addToRawHitAssociations(hit_assoc);
0470 }
0471
0472 }
0473
0474
0475
0476
0477
0478
0479 }
0480 }
0481
0482 }