Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2025-07-12 07:55:38

0001 // SPDX-License-Identifier: LGPL-3.0-or-later
0002 // Copyright (C) 2024 Derek Anderson
0003 
0004 #include <DD4hep/Detector.h>
0005 #include <edm4eic/CalorimeterHit.h>
0006 #include <edm4hep/Vector3f.h>
0007 #include <edm4hep/utils/vector_utils.h>
0008 #include <fmt/core.h>
0009 #include <podio/ObjectID.h>
0010 #include <podio/RelationRange.h>
0011 #include <cmath>
0012 #include <cstddef>
0013 #include <gsl/pointers>
0014 
0015 // algorithm definition
0016 #include "TrackClusterMergeSplitter.h"
0017 #include "algorithms/calorimetry/TrackClusterMergeSplitterConfig.h"
0018 
0019 namespace eicrecon {
0020 
0021 // --------------------------------------------------------------------------
0022 //! Initialize algorithm
0023 // --------------------------------------------------------------------------
0024 void TrackClusterMergeSplitter::init() {
0025 
0026   // grab detector id
0027   m_idCalo = m_geo.detector()->constant<int>(m_cfg.idCalo);
0028   debug("Collecting projections to detector with system id {}", m_idCalo);
0029 
0030 } // end 'init(dd4hep::Detector*)'
0031 
0032 // --------------------------------------------------------------------------
0033 //! Process inputs
0034 // --------------------------------------------------------------------------
0035 /*! Primary algorithm call: algorithm ingests a collection
0036    *  protoclusters and a collection of track projections.
0037    *  It then decides to merge or split protoclusters according
0038    *  to the following algorithm:
0039    *    1. Identify all tracks projections pointing to the
0040    *       specified calorimeter.
0041    *    2. Match relevant track projections to protoclusters
0042    *       based on distance between projection and the energy-
0043    *       weighted barycenter of the protocluster;
0044    *    3. For each cluster-track pair:
0045    *       i.  Calculate the significance of the pair's
0046    *           E/p relative to the provided mean E/p and
0047    *           its RMS; and
0048    *       ii. If the significance is less than the
0049    *           significance specified by `minSigCut`,
0050    *           merge all clusters within `drAdd`.
0051    *    4. Create a protocluster for each merged cluster
0052    *       and copy all unused protoclusters into output.
0053    *       - If multiple tracks point to the same merged
0054    *         cluster, create a new protocluster for each
0055    *         projection with hit weighted relative to
0056    *         the track momentum.
0057    */
0058 void TrackClusterMergeSplitter::process(const TrackClusterMergeSplitter::Input& input,
0059                                         const TrackClusterMergeSplitter::Output& output) const {
0060 
0061   // grab inputs/outputs
0062   const auto [in_protoclusters, in_projections] = input;
0063   auto [out_protoclusters]                      = output;
0064 
0065   // exit if no clusters in collection
0066   if (in_protoclusters->empty()) {
0067     debug("No proto-clusters in input collection.");
0068     return;
0069   }
0070 
0071   // ------------------------------------------------------------------------
0072   // 1. Identify projections to calorimeter
0073   // ------------------------------------------------------------------------
0074   VecProj vecProject;
0075   get_projections(in_projections, vecProject);
0076 
0077   // ------------------------------------------------------------------------
0078   // 2. Match relevant projections to clusters
0079   // ------------------------------------------------------------------------
0080   MapToVecProj mapProjToSplit;
0081   if (vecProject.empty()) {
0082     debug("No projections to match clusters to.");
0083     return;
0084   }
0085   match_clusters_to_tracks(in_protoclusters, vecProject, mapProjToSplit);
0086 
0087   // ------------------------------------------------------------------------
0088   // 3. Loop over projection-cluster pairs to check if merging is needed
0089   // ------------------------------------------------------------------------
0090   SetClust setUsedClust;
0091   MapToVecClust mapClustToMerge;
0092   for (auto& [clustSeed, vecMatchProj] : mapProjToSplit) {
0093 
0094     // at this point, track-cluster matches are 1-to-1
0095     // so grab matched track
0096     auto projSeed = vecMatchProj.front();
0097 
0098     // skip if cluster is already used
0099     if (setUsedClust.contains(clustSeed)) {
0100       continue;
0101     }
0102 
0103     // add cluster to list and flag as used
0104     mapClustToMerge[clustSeed].push_back(clustSeed);
0105     setUsedClust.insert(clustSeed);
0106 
0107     // grab cluster energy and projection momentum
0108     const float eClustSeed = get_cluster_energy(clustSeed);
0109     const float eProjSeed  = m_cfg.avgEP * edm4hep::utils::magnitude(projSeed.momentum);
0110 
0111     // ----------------------------------------------------------------------
0112     // 3.i. Calculate significance
0113     // ----------------------------------------------------------------------
0114     const float sigSeed = (eClustSeed - eProjSeed) / m_cfg.sigEP;
0115     trace("Seed energy = {}, expected energy = {}, significance = {}", eClustSeed, eProjSeed,
0116           sigSeed);
0117 
0118     // ----------------------------------------------------------------------
0119     // 3.ii. If significance is above threshold, do nothing.
0120     //       Otherwise identify clusters to merge.
0121     // ----------------------------------------------------------------------
0122     if (sigSeed > m_cfg.minSigCut) {
0123       continue;
0124     }
0125 
0126     // get eta, phi of seed
0127     const auto posSeed  = get_cluster_position(clustSeed);
0128     const float etaSeed = edm4hep::utils::eta(posSeed);
0129     const float phiSeed = edm4hep::utils::angleAzimuthal(posSeed);
0130 
0131     // loop over other clusters
0132     float eClustSum = eClustSeed;
0133     float sigSum    = sigSeed;
0134     for (auto in_cluster : *in_protoclusters) {
0135 
0136       // ignore used clusters
0137       if (setUsedClust.contains(in_cluster)) {
0138         continue;
0139       }
0140 
0141       // get eta, phi of cluster
0142       const auto posClust  = get_cluster_position(in_cluster);
0143       const float etaClust = edm4hep::utils::eta(posClust);
0144       const float phiClust = edm4hep::utils::angleAzimuthal(posClust);
0145 
0146       // get distance to seed
0147       const float drToSeed =
0148           std::hypot(etaSeed - etaClust, std::remainder(phiSeed - phiClust, 2. * M_PI));
0149 
0150       // --------------------------------------------------------------------
0151       // If inside merging-window, add to list of clusters to merge
0152       // --------------------------------------------------------------------
0153       if (drToSeed > m_cfg.drAdd) {
0154         continue;
0155       }
0156       mapClustToMerge[clustSeed].push_back(in_cluster);
0157       setUsedClust.insert(in_cluster);
0158 
0159       // --------------------------------------------------------------------
0160       // if picked up cluster w/ matched track, add projection to list
0161       // --------------------------------------------------------------------
0162       if (mapProjToSplit.contains(in_cluster)) {
0163         vecMatchProj.insert(vecMatchProj.end(), mapProjToSplit[in_cluster].begin(),
0164                             mapProjToSplit[in_cluster].end());
0165       }
0166 
0167       // increment sums and output debugging
0168       eClustSum += get_cluster_energy(in_cluster);
0169       sigSum = (eClustSum - eProjSeed) / m_cfg.sigEP;
0170       trace("{} clusters to merge: current sum = {}, current significance = {}, {} track(s) "
0171             "pointing to merged cluster",
0172             mapClustToMerge[clustSeed].size(), eClustSum, sigSum, vecMatchProj.size());
0173     } // end cluster loop
0174   }   // end matched cluster-projection loop
0175 
0176   // ------------------------------------------------------------------------
0177   // 4. Create an output protocluster for each merged cluster and for
0178   //    each track pointing to merged cluster
0179   // ------------------------------------------------------------------------
0180   for (auto& [clustSeed, vecClustToMerge] : mapClustToMerge) {
0181     merge_and_split_clusters(vecClustToMerge, mapProjToSplit[clustSeed], out_protoclusters);
0182   } // end clusters to merge loop
0183 
0184   // ------------------------------------------------------------------------
0185   // copy unused clusters to output
0186   // ------------------------------------------------------------------------
0187   for (auto in_cluster : *in_protoclusters) {
0188 
0189     // ignore used clusters
0190     if (setUsedClust.contains(in_cluster)) {
0191       continue;
0192     }
0193 
0194     // copy cluster and add to output collection
0195     edm4eic::MutableProtoCluster out_cluster = in_cluster.clone();
0196     out_protoclusters->push_back(out_cluster);
0197     trace("Copied input cluster {} onto output cluster {}", in_cluster.getObjectID().index,
0198           out_cluster.getObjectID().index);
0199 
0200   } // end cluster loop
0201 
0202 } // end 'process(Input&, Output&)'
0203 
0204 // --------------------------------------------------------------------------
0205 //! Collect projections pointing to calorimeter
0206 // --------------------------------------------------------------------------
0207 void TrackClusterMergeSplitter::get_projections(const edm4eic::TrackSegmentCollection* projections,
0208                                                 VecProj& relevant_projects) const {
0209 
0210   // return if projections are empty
0211   if (projections->empty()) {
0212     debug("No projections in input collection.");
0213     return;
0214   }
0215 
0216   // collect projections
0217   for (auto project : *projections) {
0218     for (auto point : project.getPoints()) {
0219       if ((point.system == m_idCalo) && (point.surface == 1)) {
0220         relevant_projects.push_back(point);
0221       }
0222     } // end point loop
0223   }   // end projection loop
0224   debug("Collected relevant projections: {} to process", relevant_projects.size());
0225 
0226 } // end 'get_projections(edm4eic::CalorimeterHit&, edm4eic::TrackSegmentCollection&, VecTrkPoint&)'
0227 
0228 // --------------------------------------------------------------------------
0229 //! Match clusters to track projections
0230 // --------------------------------------------------------------------------
0231 /*! FIXME remove this once cluster-track matching has been centralized
0232    */
0233 void TrackClusterMergeSplitter::match_clusters_to_tracks(
0234     const edm4eic::ProtoClusterCollection* clusters, const VecProj& projections,
0235     MapToVecProj& matches) const {
0236 
0237   // loop over relevant projections
0238   for (auto project : projections) {
0239 
0240     // grab projection
0241     // get eta, phi of projection
0242     const float etaProj = edm4hep::utils::eta(project.position);
0243     const float phiProj = edm4hep::utils::angleAzimuthal(project.position);
0244 
0245     // to store matched cluster
0246     edm4eic::ProtoCluster match;
0247 
0248     // find closest cluster
0249     bool foundMatch = false;
0250     float dMatch    = m_cfg.drAdd;
0251     for (auto cluster : *clusters) {
0252 
0253       // get eta, phi of cluster
0254       const auto posClust  = get_cluster_position(cluster);
0255       const float etaClust = edm4hep::utils::eta(posClust);
0256       const float phiClust = edm4hep::utils::angleAzimuthal(posClust);
0257 
0258       // calculate distance to centroid
0259       const float dist =
0260           std::hypot(etaProj - etaClust, std::remainder(phiProj - phiClust, 2. * M_PI));
0261 
0262       // if closer, set match to current projection
0263       if (dist <= dMatch) {
0264         foundMatch = true;
0265         dMatch     = dist;
0266         match      = cluster;
0267       }
0268     } // end cluster loop
0269 
0270     // record match if found
0271     if (foundMatch) {
0272       matches[match].push_back(project);
0273       trace("Matched cluster to track projection: eta-phi distance = {}", dMatch);
0274     }
0275   } // end cluster loop
0276   debug("Finished matching clusters to track projections: {} matches", matches.size());
0277 
0278 } // end 'match_clusters_to_tracks(edm4eic::ClusterCollection*, VecTrkPoint&, MapToVecProj&)'
0279 
0280 // --------------------------------------------------------------------------
0281 //! Merge identified clusters and split if needed
0282 // --------------------------------------------------------------------------
0283 /*! If multiple tracks are pointing to merged cluster, a new protocluster
0284    *  is created for each track w/ hits weighted by its distance to the track
0285    *  and the track's momentum.
0286    */
0287 void TrackClusterMergeSplitter::merge_and_split_clusters(
0288     const VecClust& to_merge, const VecProj& to_split,
0289     edm4eic::ProtoClusterCollection* out_protoclusters) const {
0290 
0291   // if only 1 matched track, no need to split
0292   if (to_split.size() == 1) {
0293     edm4eic::MutableProtoCluster new_clust = out_protoclusters->create();
0294     for (const auto& old_clust : to_merge) {
0295       for (const auto& hit : old_clust.getHits()) {
0296         new_clust.addToHits(hit);
0297         new_clust.addToWeights(1.);
0298       }
0299       trace("Merged input cluster {} into output cluster {}", old_clust.getObjectID().index,
0300             new_clust.getObjectID().index);
0301     }
0302     return;
0303   }
0304 
0305   // otherwise split merged cluster for each matched track
0306   std::vector<edm4eic::MutableProtoCluster> new_clusters;
0307   for (const auto& proj [[maybe_unused]] : to_split) {
0308     new_clusters.push_back(out_protoclusters->create());
0309   }
0310   trace("Splitting merged cluster across {} tracks", to_split.size());
0311 
0312   // loop over all hits from all clusters to merge
0313   std::vector<float> weights(to_split.size(), 1.);
0314   for (const auto& old_clust : to_merge) {
0315     for (const auto& hit : old_clust.getHits()) {
0316 
0317       // calculate hit's weight for each track
0318       for (std::size_t iProj = 0; const auto& proj : to_split) {
0319 
0320         // get track eta, phi
0321         const float etaProj = edm4hep::utils::eta(proj.position);
0322         const float phiProj = edm4hep::utils::angleAzimuthal(proj.position);
0323 
0324         // get hit eta, phi
0325         const float etaHit = edm4hep::utils::eta(hit.getPosition());
0326         const float phiHit = edm4hep::utils::angleAzimuthal(hit.getPosition());
0327 
0328         // get track momentum, distance to hit
0329         const float mom = edm4hep::utils::magnitude(proj.momentum);
0330         const float dist =
0331             std::hypot(etaHit - etaProj, std::remainder(phiHit - phiProj, 2. * M_PI));
0332 
0333         // set weight
0334         weights[iProj] = std::exp(-1. * dist / m_cfg.transverseEnergyProfileScale) * mom;
0335         ++iProj;
0336       }
0337 
0338       // normalize weights
0339       float wTotal = 0.;
0340       for (const float weight : weights) {
0341         wTotal += weight;
0342       }
0343       for (float& weight : weights) {
0344         weight /= wTotal;
0345       }
0346 
0347       // add hit to each split merged cluster w/ relevant weight
0348       for (std::size_t iProj = 0; auto& new_clust : new_clusters) {
0349         new_clust.addToHits(hit);
0350         new_clust.addToWeights(weights[iProj]);
0351       }
0352 
0353     } // end hits to merge loop
0354   }   // end clusters to merge loop
0355 
0356 } // end 'merge_and_split_clusters(VecClust&, VecProj&, edm4eic::MutableCluster&)'
0357 
0358 // --------------------------------------------------------------------------
0359 //! Grab current energy of protocluster
0360 // --------------------------------------------------------------------------
0361 float TrackClusterMergeSplitter::get_cluster_energy(const edm4eic::ProtoCluster& clust) const {
0362 
0363   float eClust = 0.;
0364   for (auto hit : clust.getHits()) {
0365     eClust += hit.getEnergy();
0366   }
0367   return eClust / m_cfg.sampFrac;
0368 
0369 } // end 'get_cluster_energy(edm4eic::ProtoCluster&)'
0370 
0371 // --------------------------------------------------------------------------
0372 //! Get current center of protocluster
0373 // --------------------------------------------------------------------------
0374 edm4hep::Vector3f
0375 TrackClusterMergeSplitter::get_cluster_position(const edm4eic::ProtoCluster& clust) const {
0376 
0377   // grab total energy
0378   const float eClust = get_cluster_energy(clust) * m_cfg.sampFrac;
0379 
0380   // calculate energy-weighted center
0381   float wTotal = 0.;
0382   edm4hep::Vector3f position(0., 0., 0.);
0383   for (auto hit : clust.getHits()) {
0384 
0385     // calculate weight
0386     float weight = hit.getEnergy() / eClust;
0387     wTotal += weight;
0388 
0389     // update cluster position
0390     position = position + (hit.getPosition() * weight);
0391   }
0392 
0393   float norm = 1.;
0394   if (wTotal == 0.) {
0395     warning("Total weight of 0 in position calculation!");
0396   } else {
0397     norm = wTotal;
0398   }
0399   return position / norm;
0400 
0401 } // end 'get_cluster_position(edm4eic::ProtoCluster&)'
0402 
0403 } // namespace eicrecon