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