Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-09-22 08:05:50

0001 // SPDX-License-Identifier: LGPL-3.0-or-later
0002 // Copyright (C) 2026 Derek Anderson, Dmitry Kalinkin
0003 
0004 #include <edm4eic/Track.h>
0005 #include <edm4eic/TrackPoint.h>
0006 #include <edm4hep/Vector3f.h>
0007 #include <edm4hep/utils/vector_utils.h>
0008 #include <podio/ObjectID.h>
0009 #include <podio/RelationRange.h>
0010 #include <podio/detail/Link.h>
0011 #include <podio/detail/LinkCollectionImpl.h>
0012 #include <cmath>
0013 #include <cstddef>
0014 #include <memory>
0015 #include <set>
0016 #include <tuple>
0017 #include <utility>
0018 
0019 #include "TrackClusterMergeSplitter.h"
0020 #include "algorithms/calorimetry/TrackClusterMergeSplitterConfig.h"
0021 
0022 namespace eicrecon {
0023 
0024 /*! Merges and splits clusters based on matched tracks
0025  *  according to the following algorithm:
0026  *    1. Build map of clusters onto matched track
0027  *       projections.
0028  *    2. For each cluster-track pair:
0029  *       a.  Calculate significance of pair's E/p wrt provided
0030  *           average and RMS of E/p
0031  *       b. If significance is less than `minSigCut`,
0032  *           merge all clusters within `drAdd`.
0033  *    3. Create a protocluster for each merged cluster
0034  *       - If multiple tracks point to same merged
0035  *         protocluster, create new protocluster for
0036  *         each projection with hits weighted relative
0037  *         to track.
0038  *    4. Convert any unmatched clusters into protoclusters.
0039  */
0040 void TrackClusterMergeSplitter::process(const TrackClusterMergeSplitter::Input& input,
0041                                         const TrackClusterMergeSplitter::Output& output) const {
0042 
0043   // grab inputs/outputs
0044   const auto [in_matches, in_clusters, in_projections] = input;
0045   auto [out_protos, out_links]                         = output;
0046 
0047   // exit if no clusters in collection
0048   if (in_clusters->empty()) {
0049     debug("No clusters in input collection.");
0050     return;
0051   }
0052 
0053   // emit debugging message if no matched tracks in collection
0054   if (in_matches->empty()) {
0055     debug("No matched tracks in collection.");
0056     return;
0057   }
0058 
0059   // --------------------------------------------------------------------------
0060   // 1. Build map of clusters onto tracks/projections
0061   // --------------------------------------------------------------------------
0062   std::map<edm4eic::Cluster, segment_vector, CompareObjectID<edm4eic::Cluster>> mapProjToSplit;
0063   for (const auto& match : *in_matches) {
0064     for (const auto& project : *in_projections) {
0065 
0066       // pick out corresponding projection from track
0067       if (match.getTrack() != project.getTrack()) {
0068         continue;
0069       }
0070       mapProjToSplit[match.getCluster()].push_back(project);
0071     }
0072   } // end track-cluster match loop
0073 
0074   // square merging-window radius to avoid std::sqrt in comparison
0075   const float drAdd2 = m_cfg.drAdd * m_cfg.drAdd;
0076   trace("Squared radius of merging window: radius = {}, radius^2 = {}", m_cfg.drAdd, drAdd2);
0077 
0078   // ------------------------------------------------------------------------
0079   // 2. Loop over projection-cluster pairs to check if merging is needed
0080   // ------------------------------------------------------------------------
0081   std::set<edm4eic::Cluster> setUsedClust;
0082   std::map<edm4eic::Cluster, cluster_vector, CompareObjectID<edm4eic::Cluster>> mapClustToMerge;
0083   for (auto& [clust_seed, vecMatchProj] : mapProjToSplit) {
0084 
0085     // at this point, track-cluster matches are 1-to-1
0086     // so grab matched track and get its projection to
0087     // a specific point
0088     std::optional<edm4eic::TrackPoint> project_seed;
0089     for (auto point : vecMatchProj.front().getPoints()) {
0090       if (point.surface == m_cfg.surfaceToUse) {
0091         project_seed = point;
0092       }
0093     }
0094     if (!project_seed) {
0095       continue;
0096     }
0097 
0098     // skip if cluster is already used
0099     if (setUsedClust.contains(clust_seed)) {
0100       continue;
0101     }
0102 
0103     // grab cluster energy and projection momentum
0104     const float eClustSeed = clust_seed.getEnergy();
0105     const float eProjSeed  = m_cfg.avgEP * edm4hep::utils::magnitude(project_seed.value().momentum);
0106 
0107     // ----------------------------------------------------------------------
0108     // 2(a). Calculate significance
0109     // ----------------------------------------------------------------------
0110     const float sigSeed = (eClustSeed - eProjSeed) / m_cfg.sigEP;
0111     trace("Seed energy = {}, expected energy = {}, significance = {}", eClustSeed, eProjSeed,
0112           sigSeed);
0113 
0114     // ----------------------------------------------------------------------
0115     // 2(b). If significance is above threshold, do nothing.
0116     //       Otherwise identify clusters to merge.
0117     // ----------------------------------------------------------------------
0118     if (sigSeed > m_cfg.minSigCut) {
0119       continue;
0120     }
0121 
0122     // get eta, phi of seed
0123     const float etaSeed = edm4hep::utils::eta(clust_seed.getPosition());
0124     const float phiSeed = edm4hep::utils::angleAzimuthal(clust_seed.getPosition());
0125 
0126     // loop over other clusters
0127     float eClustSum = eClustSeed;
0128     for (auto cluster : *in_clusters) {
0129 
0130       // ignore used clusters
0131       if (setUsedClust.contains(cluster)) {
0132         continue;
0133       }
0134 
0135       // don't double count seed cluster
0136       if (clust_seed == cluster) {
0137         continue;
0138       }
0139 
0140       // get eta, phi of cluster
0141       const float etaClust = edm4hep::utils::eta(cluster.getPosition());
0142       const float phiClust = edm4hep::utils::angleAzimuthal(cluster.getPosition());
0143 
0144       // get distance to seed
0145       const float dEtaToSeed = etaSeed - etaClust;
0146       const float dPhiToSeed = std::remainder(phiSeed - phiClust, 2. * M_PI);
0147       const float drToSeed2  = (dEtaToSeed * dEtaToSeed) + (dPhiToSeed * dPhiToSeed);
0148       trace("Distances from cluster to seed: dEta = {}, dPhi = {}, dr^2 = {}", dEtaToSeed,
0149             dPhiToSeed, drToSeed2);
0150 
0151       // --------------------------------------------------------------------
0152       // If inside merging-window, add to list of clusters to merge
0153       // --------------------------------------------------------------------
0154       if (drToSeed2 > drAdd2) {
0155         continue;
0156       }
0157       mapClustToMerge[clust_seed].push_back(cluster);
0158       setUsedClust.insert(cluster);
0159       eClustSum += cluster.getEnergy();
0160 
0161       // --------------------------------------------------------------------
0162       // if picked up cluster w/ matched track, add projection to list
0163       // --------------------------------------------------------------------
0164       if (mapProjToSplit.contains(cluster)) {
0165         vecMatchProj.insert(vecMatchProj.end(), mapProjToSplit[cluster].begin(),
0166                             mapProjToSplit[cluster].end());
0167       }
0168 
0169       const float sigSum = (eClustSum - eProjSeed) / m_cfg.sigEP;
0170       trace("{} clusters to merge: current sum = {}, current significance = {}, {} track(s) "
0171             "pointing to merged cluster",
0172             mapClustToMerge[clust_seed].size(), eClustSum, sigSum, vecMatchProj.size());
0173     } // end cluster loop
0174 
0175     // if found clusters to merge, flag seed as used
0176     if (mapClustToMerge.count(clust_seed) > 0) {
0177       setUsedClust.insert(clust_seed);
0178     }
0179 
0180   } // end matched cluster-projection loop
0181 
0182   // ------------------------------------------------------------------------
0183   // 3. Create an output protocluster for each merged cluster
0184   //    and for each track pointing to merged cluster
0185   // ------------------------------------------------------------------------
0186   for (auto& [clust_seed, vecClustToMerge] : mapClustToMerge) {
0187 
0188     // create a cluster for each projection to merged cluster
0189     protocluster_vector new_protos;
0190     for ([[maybe_unused]] const auto& project : mapProjToSplit[clust_seed]) {
0191       new_protos.push_back(out_protos->create());
0192     }
0193 
0194     vecClustToMerge.push_back(clust_seed);
0195     merge_and_split_clusters(vecClustToMerge, mapProjToSplit[clust_seed], new_protos);
0196 
0197     // and finally create a track-protocluster link for each pair
0198     for (std::size_t iProj = 0; const auto& project : mapProjToSplit[clust_seed]) {
0199       auto link = out_links->create();
0200       link.setTo(new_protos[iProj]);
0201       link.setFrom(project.getTrack());
0202       link.setWeight(1.0); // FIXME placeholder, should encode goodness of match
0203       trace("Matched output cluster {} to track {}", new_protos[iProj].getObjectID().index,
0204             project.getTrack().getObjectID().index);
0205       ++iProj;
0206     }
0207   } // end clusters to merge loop
0208 
0209   // ------------------------------------------------------------------------
0210   // 4. Convert unused clusters to protoclusters
0211   // ------------------------------------------------------------------------
0212   for (const auto& cluster : *in_clusters) {
0213 
0214     // ignore clusters used during merging
0215     if (setUsedClust.contains(cluster)) {
0216       continue;
0217     }
0218 
0219     // copy cluster and add to output collection
0220     edm4eic::MutableProtoCluster proto = out_protos->create();
0221     add_cluster_to_proto(cluster, proto);
0222     trace("Copied input cluster {} onto output cluster {}", cluster.getObjectID().index,
0223           proto.getObjectID().index);
0224 
0225   } // end cluster loop
0226 
0227 } // end 'process(Input&, Output&)'
0228 
0229 /*! If multiple tracks are pointing to merged cluster, a new
0230  *  protocluster is created for each track w/ hits weighted by
0231  *  its distance to the track and the track's momentum.
0232  */
0233 void TrackClusterMergeSplitter::merge_and_split_clusters(const cluster_vector& to_merge,
0234                                                          const segment_vector& to_split,
0235                                                          protocluster_vector& new_protos) const {
0236 
0237   // if only 1 matched track, no need to split
0238   // otherwise split merged cluster for each
0239   // matched track
0240   if (to_split.size() == 1) {
0241     for (const auto& old_clust : to_merge) {
0242       add_cluster_to_proto(old_clust, new_protos.front());
0243     }
0244     return;
0245   }
0246   trace("Splitting merged cluster across {} tracks", to_split.size());
0247 
0248   // calculate weights for splitting
0249   std::vector<hit_to_weight_map> weights(to_split.size());
0250   for (const auto& old_clust : to_merge) {
0251     for (const auto& hit : old_clust.getHits()) {
0252 
0253       // calculate a weight for each projection
0254       double wTotal = 0.;
0255       for (std::size_t iProj = 0; const auto& projToSplit : to_split) {
0256 
0257         // get track at specific point
0258         std::optional<edm4eic::TrackPoint> proj;
0259         for (auto point : projToSplit.getPoints()) {
0260           if (point.surface == m_cfg.surfaceToUse) {
0261             proj = point;
0262           }
0263         }
0264         if (!proj) {
0265           continue;
0266         }
0267 
0268         // get track eta, phi
0269         const float etaProj = edm4hep::utils::eta(proj.value().position);
0270         const float phiProj = edm4hep::utils::angleAzimuthal(proj.value().position);
0271 
0272         // get hit eta, phi
0273         const float etaHit = edm4hep::utils::eta(hit.getPosition());
0274         const float phiHit = edm4hep::utils::angleAzimuthal(hit.getPosition());
0275 
0276         // get track momentum, distance to hit
0277         const float mom = edm4hep::utils::magnitude(proj.value().momentum);
0278         const float dist =
0279             std::hypot(etaHit - etaProj, std::remainder(phiHit - phiProj, 2. * M_PI));
0280 
0281         // get weight
0282         const float weight = std::exp(-1. * dist / m_cfg.transverseEnergyProfileScale) * mom;
0283 
0284         // set weight & increment sum of weights
0285         weights[iProj][hit] = weight;
0286         wTotal += weight;
0287         ++iProj;
0288       }
0289 
0290       // normalize weights over all projections
0291       for (std::size_t iProj = 0; iProj < to_split.size(); ++iProj) {
0292         weights[iProj][hit] /= wTotal;
0293       }
0294     } // end hits to merge loop
0295 
0296     // merge cluster into split
0297     for (std::size_t iProj = 0; iProj < to_split.size(); ++iProj) {
0298       add_cluster_to_proto(old_clust, new_protos[iProj], weights[iProj]);
0299     }
0300   } // end clusters to merge loop
0301 
0302 } // end 'merge_and_split_clusters(cluster_vector&, segment_vector&, std::vector<edm4eic::MutableProtoCluster>&)'
0303 
0304 /*! Adds a cluster's hits to a protocluster. If provided,
0305  *  will also set weight of hit based on the map `split_weights`.
0306  */
0307 void TrackClusterMergeSplitter::add_cluster_to_proto(
0308     const edm4eic::Cluster& clust, edm4eic::MutableProtoCluster& proto,
0309     std::optional<hit_to_weight_map> split_weights) {
0310 
0311   // loop over hits to add
0312   for (const auto& hit : clust.getHits()) {
0313 
0314     // get weight if needed
0315     double weight = 1.0;
0316     if (split_weights.has_value()) {
0317       weight = split_weights.value()[hit];
0318     }
0319 
0320     // add to protocluster
0321     proto.addToHits(hit);
0322     proto.addToWeights(weight);
0323   } // end hit loop
0324 
0325 } // end 'add_cluster_to_proto(...)'
0326 
0327 } // namespace eicrecon