Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-06-17 07:50:49

0001 // SPDX-License-Identifier: LGPL-3.0-or-later
0002 // Copyright (C) 2026 Sebouh Paul, Baptiste Fraisse
0003 
0004 #include <Evaluator/DD4hepUnits.h>
0005 #include <TVector3.h>
0006 #include <edm4eic/Cluster.h>
0007 #include <edm4eic/ReconstructedParticleCollection.h>
0008 #include <edm4eic/Vertex.h>
0009 #include <edm4hep/Vector3f.h>
0010 #include <stdint.h>
0011 #include <algorithm>
0012 #include <array>
0013 #include <cmath>
0014 #include <cstddef>
0015 #include <stdexcept>
0016 #include <tuple>
0017 #include <vector>
0018 
0019 #include "LambdaReconstruction.h"
0020 #include "TLorentzVector.h"
0021 
0022 namespace eicrecon {
0023 
0024 void LambdaReconstruction::init() {
0025   try {
0026     m_zMax = m_detector->constant<double>(m_cfg.offsetPositionName);
0027   } catch (std::runtime_error&) {
0028     m_zMax = 35800;
0029     trace("Failed to get {} from the detector, using default value of {}", m_cfg.offsetPositionName,
0030           m_zMax);
0031   }
0032 }
0033 
0034 // reconstruction machinery from n+g+g
0035 
0036 bool LambdaReconstruction::reconstruct_from_triplet(
0037     const edm4eic::ReconstructedParticle& n_in, const edm4eic::ReconstructedParticle& g1_in,
0038     const edm4eic::ReconstructedParticle& g2_in,
0039     edm4eic::ReconstructedParticleCollection* out_lambdas,
0040     edm4eic::ReconstructedParticleCollection* out_decay_products) const {
0041   static const double m_neutron = m_particleSvc.particle(2112).mass;
0042   static const double m_pi0     = m_particleSvc.particle(111).mass;
0043   static const double m_lambda  = m_particleSvc.particle(3122).mass;
0044 
0045   const double En = n_in.getEnergy();
0046   const double E1 = g1_in.getEnergy();
0047   const double E2 = g2_in.getEnergy();
0048 
0049   if (En <= m_neutron) {
0050     return false;
0051   }
0052   const double pn = std::sqrt(std::max(0.0, En * En - m_neutron * m_neutron));
0053 
0054   TVector3 xn;
0055   TVector3 x1;
0056   TVector3 x2;
0057 
0058   if (n_in.clusters_size() == 0 || g1_in.clusters_size() == 0 || g2_in.clusters_size() == 0) {
0059     return false;
0060   }
0061 
0062   const auto rn  = n_in.getClusters(0).getPosition();
0063   const auto rg1 = g1_in.getClusters(0).getPosition();
0064   const auto rg2 = g2_in.getClusters(0).getPosition();
0065 
0066   xn.SetXYZ(rn.x * dd4hep::mm, rn.y * dd4hep::mm, rn.z * dd4hep::mm);
0067   x1.SetXYZ(rg1.x * dd4hep::mm, rg1.y * dd4hep::mm, rg1.z * dd4hep::mm);
0068   x2.SetXYZ(rg2.x * dd4hep::mm, rg2.y * dd4hep::mm, rg2.z * dd4hep::mm);
0069 
0070   xn.RotateY(-m_cfg.globalToProtonRotation);
0071   x1.RotateY(-m_cfg.globalToProtonRotation);
0072   x2.RotateY(-m_cfg.globalToProtonRotation);
0073 
0074   TVector3 vtx(0, 0, 0);
0075   double f  = 0.0;
0076   double df = 0.5;
0077 
0078   const double theta_open_expected = 2 * std::asin(m_pi0 / (2 * std::sqrt(E1 * E2)));
0079 
0080   TLorentzVector n;
0081   TLorentzVector g1;
0082   TLorentzVector g2;
0083   TLorentzVector lambda;
0084 
0085   for (int i = 0; i < m_cfg.iterations; i++) {
0086     n      = {pn * (xn - vtx).Unit(), En};
0087     g1     = {E1 * (x1 - vtx).Unit(), E1};
0088     g2     = {E2 * (x2 - vtx).Unit(), E2};
0089     lambda = n + g1 + g2;
0090 
0091     const double theta_open = g1.Angle(g2.Vect());
0092     if (theta_open > theta_open_expected) {
0093       f -= df;
0094     } else if (theta_open < theta_open_expected) {
0095       f += df;
0096     }
0097 
0098     vtx = lambda.Vect() * (f * m_zMax / lambda.Z());
0099     df /= 2.0;
0100   }
0101 
0102   const double mass_rec = lambda.M();
0103   if (std::abs(mass_rec - m_lambda) > m_cfg.lambdaMassWindow * m_lambda) {
0104     return false;
0105   }
0106 
0107   // rotate everything back to lab coordinates
0108   vtx.RotateY(m_cfg.globalToProtonRotation);
0109   lambda.RotateY(m_cfg.globalToProtonRotation);
0110   n.RotateY(m_cfg.globalToProtonRotation);
0111   g1.RotateY(m_cfg.globalToProtonRotation);
0112   g2.RotateY(m_cfg.globalToProtonRotation);
0113 
0114   // boost decay products to Lambda rest frame
0115   auto b = -lambda.BoostVector();
0116   n.Boost(b);
0117   g1.Boost(b);
0118   g2.Boost(b);
0119 
0120   // vertex back to EDM units (mm)
0121   vtx = vtx * (1.0 / dd4hep::mm);
0122 
0123   // saving reco lambda
0124   auto rec_lambda = out_lambdas->create();
0125   rec_lambda.setPDG(3122);
0126   rec_lambda.setEnergy(lambda.E());
0127   rec_lambda.setMomentum({static_cast<float>(lambda.X()), static_cast<float>(lambda.Y()),
0128                           static_cast<float>(lambda.Z())});
0129   rec_lambda.setReferencePoint(
0130       {static_cast<float>(vtx.X()), static_cast<float>(vtx.Y()), static_cast<float>(vtx.Z())});
0131   rec_lambda.setCharge(0);
0132   rec_lambda.setMass(mass_rec);
0133 
0134   // --- cm neutron
0135   auto neutron_cm = out_decay_products->create();
0136   neutron_cm.setPDG(2112);
0137   neutron_cm.setEnergy(n.E());
0138   neutron_cm.setMomentum(
0139       {static_cast<float>(n.X()), static_cast<float>(n.Y()), static_cast<float>(n.Z())});
0140   neutron_cm.setReferencePoint(
0141       {static_cast<float>(vtx.X()), static_cast<float>(vtx.Y()), static_cast<float>(vtx.Z())});
0142   neutron_cm.setCharge(0);
0143   neutron_cm.setMass(m_neutron);
0144 
0145   // --- cm gammas
0146   auto gamma1_cm = out_decay_products->create();
0147   gamma1_cm.setPDG(22);
0148   gamma1_cm.setEnergy(g1.E());
0149   gamma1_cm.setMomentum(
0150       {static_cast<float>(g1.X()), static_cast<float>(g1.Y()), static_cast<float>(g1.Z())});
0151   gamma1_cm.setReferencePoint(
0152       {static_cast<float>(vtx.X()), static_cast<float>(vtx.Y()), static_cast<float>(vtx.Z())});
0153   gamma1_cm.setCharge(0);
0154   gamma1_cm.setMass(0);
0155 
0156   auto gamma2_cm = out_decay_products->create();
0157   gamma2_cm.setPDG(22);
0158   gamma2_cm.setEnergy(g2.E());
0159   gamma2_cm.setMomentum(
0160       {static_cast<float>(g2.X()), static_cast<float>(g2.Y()), static_cast<float>(g2.Z())});
0161   gamma2_cm.setReferencePoint(
0162       {static_cast<float>(vtx.X()), static_cast<float>(vtx.Y()), static_cast<float>(vtx.Z())});
0163   gamma2_cm.setCharge(0);
0164   gamma2_cm.setMass(0);
0165 
0166   rec_lambda.addToParticles(n_in);
0167   rec_lambda.addToParticles(g1_in);
0168   rec_lambda.addToParticles(g2_in);
0169 
0170   neutron_cm.addToParticles(n_in);
0171   gamma1_cm.addToParticles(g1_in);
0172   gamma2_cm.addToParticles(g2_in);
0173 
0174   return true;
0175 }
0176 
0177 void LambdaReconstruction::process(const LambdaReconstruction::Input& input,
0178                                    const LambdaReconstruction::Output& output) const {
0179   const auto [neutralsHcal, neutralsB0, neutralsEcalEndcapP, neutralsLFHCAL] = input;
0180   auto [out_lambdas, out_decay_products]                                     = output;
0181 
0182   const double m_pi0    = m_particleSvc.particle(111).mass;
0183   const double m_lambda = m_particleSvc.particle(3122).mass;
0184 
0185   // --------------------------------------------------------------------------
0186   // Step 1: collect neutral candidates by broad detector category
0187   //
0188   // We keep two categories for ranking purposes:
0189   //   - ZDC-preferred neutrals
0190   //   - other far-forward neutrals
0191   // while centralizing the detector-specific collection handling in a single
0192   // local table to avoid duplicating hardcoded loops throughout the algorithm.
0193   // --------------------------------------------------------------------------
0194 
0195   std::vector<edm4eic::ReconstructedParticle> neutrons_zdc;
0196   std::vector<edm4eic::ReconstructedParticle> neutrons_other;
0197   std::vector<edm4eic::ReconstructedParticle> gammas_zdc;
0198   std::vector<edm4eic::ReconstructedParticle> gammas_other;
0199 
0200   struct NeutralInputDescription {
0201     const edm4eic::ReconstructedParticleCollection* coll;
0202     bool use_gamma;
0203     bool use_neutron;
0204     bool is_zdc;
0205   };
0206 
0207   const std::array<NeutralInputDescription, 4> input_descs{{
0208       {neutralsHcal, true, true, true},
0209       {neutralsB0, true, false, false},
0210       {neutralsEcalEndcapP, true, false, false},
0211       {neutralsLFHCAL, false, true, false},
0212   }};
0213 
0214   for (const auto& desc : input_descs) {
0215     for (const auto& part : *desc.coll) {
0216       if (desc.use_neutron && part.getPDG() == 2112) {
0217         (desc.is_zdc ? neutrons_zdc : neutrons_other).push_back(part);
0218       }
0219       if (desc.use_gamma && part.getPDG() == 22) {
0220         (desc.is_zdc ? gammas_zdc : gammas_other).push_back(part);
0221       }
0222     }
0223   }
0224 
0225   if (neutrons_zdc.empty() && neutrons_other.empty()) {
0226     debug("No neutrons in input collection");
0227     return;
0228   }
0229 
0230   // --------------------------------------------------------------------------
0231   // Step 2: build a unified photon pool while retaining whether each photon
0232   // comes from the ZDC-preferred category.
0233   // --------------------------------------------------------------------------
0234 
0235   using ParticleT = edm4eic::ReconstructedParticle;
0236 
0237   std::vector<ParticleT> gamma_pool;
0238   std::vector<bool> gamma_is_zdc;
0239 
0240   gamma_pool.reserve(gammas_zdc.size() + gammas_other.size());
0241   gamma_is_zdc.reserve(gammas_zdc.size() + gammas_other.size());
0242 
0243   for (const auto& g : gammas_zdc) {
0244     gamma_pool.push_back(g);
0245     gamma_is_zdc.push_back(1);
0246   }
0247   for (const auto& g : gammas_other) {
0248     gamma_pool.push_back(g);
0249     gamma_is_zdc.push_back(0);
0250   }
0251 
0252   if (gamma_pool.size() < 2) {
0253     debug("Less than 2 gammas found ({})", gamma_pool.size());
0254     return;
0255   }
0256 
0257   // --------------------------------------------------------------------------
0258   // Invariant-mass helpers
0259   // --------------------------------------------------------------------------
0260 
0261   auto invMass2 = [](const ParticleT& a, const ParticleT& b) {
0262     const auto pa = a.getMomentum();
0263     const auto pb = b.getMomentum();
0264 
0265     const double Ea = a.getEnergy();
0266     const double Eb = b.getEnergy();
0267 
0268     const double px = pa.x + pb.x;
0269     const double py = pa.y + pb.y;
0270     const double pz = pa.z + pb.z;
0271     const double E  = Ea + Eb;
0272 
0273     return E * E - (px * px + py * py + pz * pz);
0274   };
0275 
0276   auto invMass2_3 = [](const ParticleT& a, const ParticleT& b, const ParticleT& c) {
0277     const auto pa = a.getMomentum();
0278     const auto pb = b.getMomentum();
0279     const auto pc = c.getMomentum();
0280 
0281     const double Ea = a.getEnergy();
0282     const double Eb = b.getEnergy();
0283     const double Ec = c.getEnergy();
0284 
0285     const double px = pa.x + pb.x + pc.x;
0286     const double py = pa.y + pb.y + pc.y;
0287     const double pz = pa.z + pb.z + pc.z;
0288     const double E  = Ea + Eb + Ec;
0289 
0290     return E * E - (px * px + py * py + pz * pz);
0291   };
0292 
0293   // --------------------------------------------------------------------------
0294   // Step 3: build pi0 candidates from photon pairs
0295   // --------------------------------------------------------------------------
0296 
0297   enum class GammaCategory : uint8_t { TwoZDC = 0, OneZDC = 1, ZeroZDC = 2 };
0298 
0299   struct Pi0Pair {
0300     int i;
0301     int j;
0302     GammaCategory cat;
0303     double dmpi0_cand;
0304   };
0305 
0306   std::vector<Pi0Pair> pi0_pairs;
0307   pi0_pairs.reserve(gamma_pool.size() * (gamma_pool.size() - 1) / 2);
0308 
0309   for (size_t i = 0; i < gamma_pool.size(); ++i) {
0310     for (size_t j = i + 1; j < gamma_pool.size(); ++j) {
0311       const auto& g1 = gamma_pool[i];
0312       const auto& g2 = gamma_pool[j];
0313 
0314       const double m2 = invMass2(g1, g2);
0315       if (m2 <= 0.0) {
0316         continue;
0317       }
0318 
0319       const double m  = std::sqrt(m2);
0320       const double dm = std::abs(m - m_pi0);
0321       if (dm > m_cfg.pi0Window * m_pi0) {
0322         continue;
0323       }
0324 
0325       const bool z1 = gamma_is_zdc[i];
0326       const bool z2 = gamma_is_zdc[j];
0327 
0328       const GammaCategory cat = (z1 && z2)   ? GammaCategory::TwoZDC
0329                                 : (z1 || z2) ? GammaCategory::OneZDC
0330                                              : GammaCategory::ZeroZDC;
0331 
0332       pi0_pairs.push_back({static_cast<int>(i), static_cast<int>(j), cat, dm});
0333     }
0334   }
0335 
0336   if (pi0_pairs.empty()) {
0337     return;
0338   }
0339 
0340   // --------------------------------------------------------------------------
0341   // Step 4: build Lambda candidate pool from pi0-neutron combinations
0342   // --------------------------------------------------------------------------
0343 
0344   enum class NeutronCategory : uint8_t { ZDC = 0, Other = 1 };
0345 
0346   struct LambdaCandidate {
0347     int g_i;
0348     int g_j;
0349     int n_idx;
0350     NeutronCategory n_cat;
0351     GammaCategory g_cat;
0352     double chi2;
0353     double E;
0354     double pz;
0355   };
0356 
0357   std::vector<LambdaCandidate> cands;
0358   cands.reserve(64); // NB <= 4^3 candidates when searching for 3 daughters across 4 calos
0359 
0360   auto try_neutrons = [&](const auto& neutron_list, NeutronCategory n_cat) {
0361     for (const auto& pp : pi0_pairs) {
0362       const auto& g1 = gamma_pool[pp.i];
0363       const auto& g2 = gamma_pool[pp.j];
0364 
0365       for (size_t in = 0; in < neutron_list.size(); ++in) {
0366         const auto& n = neutron_list[in];
0367 
0368         const double m2L = invMass2_3(n, g1, g2);
0369         if (m2L <= 0.0) {
0370           continue;
0371         }
0372 
0373         const double mL = std::sqrt(m2L);
0374         const double dL = mL - m_lambda;
0375 
0376         if (std::abs(dL) > m_cfg.lambdaMassWindow * m_lambda) {
0377           continue;
0378         }
0379 
0380         const double pi0_term    = pp.dmpi0_cand / (m_cfg.pi0Window * m_pi0);
0381         const double lambda_term = dL / (m_cfg.lambdaMassWindow * m_lambda);
0382 
0383         const double chi2 = pi0_term * pi0_term + lambda_term * lambda_term;
0384 
0385         const double E = n.getEnergy() + g1.getEnergy() + g2.getEnergy();
0386 
0387         const auto pn  = n.getMomentum();
0388         const auto pg1 = g1.getMomentum();
0389         const auto pg2 = g2.getMomentum();
0390 
0391         const double pz = pn.z + pg1.z + pg2.z;
0392 
0393         cands.push_back({pp.i, pp.j, static_cast<int>(in), n_cat, pp.cat, chi2, E, pz});
0394       }
0395     }
0396   };
0397 
0398   if (!neutrons_zdc.empty()) {
0399     try_neutrons(neutrons_zdc, NeutronCategory::ZDC);
0400   }
0401   if (!neutrons_other.empty()) {
0402     try_neutrons(neutrons_other, NeutronCategory::Other);
0403   }
0404 
0405   if (cands.empty()) {
0406     return;
0407   }
0408 
0409   // --------------------------------------------------------------------------
0410   // Step 5: rank candidates and keep the best one
0411   // Preference order:
0412   //   1. ZDC neutron over other neutron
0413   //   2. more ZDC photons in the pi0 candidate
0414   //   3. lower combined mass-based chi2
0415   //   4. larger forward momentum pz
0416   //   5. larger total energy
0417   // --------------------------------------------------------------------------
0418 
0419   auto better = [&](const LambdaCandidate& a, const LambdaCandidate& b) -> bool {
0420     if (a.n_cat != b.n_cat) {
0421       return static_cast<int>(a.n_cat) < static_cast<int>(b.n_cat);
0422     }
0423     if (a.g_cat != b.g_cat) {
0424       return static_cast<int>(a.g_cat) < static_cast<int>(b.g_cat);
0425     }
0426     if (a.chi2 != b.chi2) {
0427       return a.chi2 < b.chi2;
0428     }
0429     if (a.pz != b.pz) {
0430       return a.pz > b.pz;
0431     }
0432     return a.E > b.E;
0433   };
0434 
0435   int best_k = 0;
0436   for (int k = 1; k < static_cast<int>(cands.size()); ++k) {
0437     if (better(cands[k], cands[best_k])) {
0438       best_k = k;
0439     }
0440   }
0441 
0442   const auto& best = cands[best_k];
0443 
0444   const auto& g1 = gamma_pool[best.g_i];
0445   const auto& g2 = gamma_pool[best.g_j];
0446   const auto& n =
0447       (best.n_cat == NeutronCategory::ZDC) ? neutrons_zdc[best.n_idx] : neutrons_other[best.n_idx];
0448 
0449   reconstruct_from_triplet(n, g1, g2, out_lambdas, out_decay_products);
0450 }
0451 
0452 } // namespace eicrecon