Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2025-01-30 10:04:50

0001 // SPDX-License-Identifier: LGPL-3.0-or-later
0002 // Copyright (C) 2022 - 2024, Sylvester Joosten, Chao Peng, Wouter Deconinck, David Lawrence, Derek Anderson
0003 
0004 /*
0005  *  Reconstruct the cluster/layer info for imaging calorimeter
0006  *  Logarithmic weighting is used to describe energy deposit in transverse direction
0007  *
0008  *  Author: Chao Peng (ANL), 06/02/2021
0009  */
0010 
0011 #pragma once
0012 
0013 #include <Eigen/Dense>
0014 #include <algorithm>
0015 
0016 #include <algorithms/algorithm.h>
0017 #include <DDRec/CellIDPositionConverter.h>
0018 #include <DDRec/Surface.h>
0019 #include <DDRec/SurfaceManager.h>
0020 
0021 #include "algorithms/calorimetry/ClusterTypes.h"
0022 
0023 // Event Model related classes
0024 #include <edm4hep/MCParticleCollection.h>
0025 #if EDM4EIC_VERSION_MAJOR >= 7
0026 #include <edm4eic/MCRecoCalorimeterHitAssociationCollection.h>
0027 #else
0028 #include <edm4hep/SimCalorimeterHitCollection.h>
0029 #endif
0030 #include <edm4hep/utils/vector_utils.h>
0031 #include <edm4eic/CalorimeterHitCollection.h>
0032 #include <edm4eic/ClusterCollection.h>
0033 #include <edm4eic/MCRecoClusterParticleAssociationCollection.h>
0034 #include <edm4eic/ProtoClusterCollection.h>
0035 
0036 #include "algorithms/interfaces/WithPodConfig.h"
0037 #include "ImagingClusterRecoConfig.h"
0038 
0039 namespace eicrecon {
0040 
0041   using ImagingClusterRecoAlgorithm = algorithms::Algorithm<
0042     algorithms::Input<
0043       edm4eic::ProtoClusterCollection,
0044 #if EDM4EIC_VERSION_MAJOR >= 7
0045       edm4eic::MCRecoCalorimeterHitAssociationCollection
0046 #else
0047       edm4hep::SimCalorimeterHitCollection
0048 #endif
0049     >,
0050     algorithms::Output<
0051       edm4eic::ClusterCollection,
0052       edm4eic::MCRecoClusterParticleAssociationCollection,
0053       edm4eic::ClusterCollection
0054     >
0055   >;
0056 
0057   /** Imaging cluster reconstruction.
0058    *
0059    *  Reconstruct the cluster/layer info for imaging calorimeter
0060    *  Logarithmic weighting is used to describe energy deposit in transverse direction
0061    *
0062    *  \ingroup reco
0063    */
0064   class ImagingClusterReco
0065       : public ImagingClusterRecoAlgorithm,
0066         public WithPodConfig<ImagingClusterRecoConfig> {
0067 
0068   public:
0069     ImagingClusterReco(std::string_view name)
0070       : ImagingClusterRecoAlgorithm{name,
0071 #if EDM4EIC_VERSION_MAJOR >= 7
0072                             {"inputProtoClusterCollection", "mcRawHitAssocations"},
0073 #else
0074                             {"inputProtoClusterCollection", "mcHits"},
0075 #endif
0076                             {"outputClusterCollection", "outputClusterAssociations", "outputLayerCollection"},
0077                             "Reconstruct the cluster/layer info for imaging calorimeter."} {}
0078 
0079   public:
0080 
0081     void init()  { }
0082 
0083     void process(const Input& input, const Output& output) const final {
0084 
0085 #if EDM4EIC_VERSION_MAJOR >= 7
0086         const auto [proto, mchitassociations] = input;
0087 #else
0088         const auto [proto, mchits] = input;
0089 #endif
0090         auto [clusters, associations, layers] = output;
0091 
0092         for (const auto& pcl: *proto) {
0093             if (!pcl.getHits().empty() && !pcl.getHits(0).isAvailable()) {
0094                 warning("Protocluster hit relation is invalid, skipping protocluster");
0095                 continue;
0096             }
0097             // get cluster and associated layers
0098             auto cl = reconstruct_cluster(pcl);
0099             auto cl_layers = reconstruct_cluster_layers(pcl);
0100 
0101             // Get cluster direction from the layer profile
0102             auto [theta, phi] = fit_track(cl_layers);
0103             cl.setIntrinsicTheta(theta);
0104             cl.setIntrinsicPhi(phi);
0105             // no error on the intrinsic direction TODO
0106 
0107             // store layer and clusters on the datastore
0108             for (const auto& layer: cl_layers) {
0109                 layers->push_back(layer);
0110                 cl.addToClusters(layer);
0111             }
0112             clusters->push_back(cl);
0113 
0114             // If sim hits are available, associate cluster with MCParticle
0115 #if EDM4EIC_VERSION_MAJOR >= 7
0116             if (mchitassociations->size() == 0) {
0117                 debug("Provided MCRecoCalorimeterHitAssociation collection is empty. No truth associations will be performed.");
0118                 continue;
0119             } else {
0120                 associate_mc_particles(cl, mchitassociations, associations);
0121             }
0122 #else
0123             if (mchits->size() == 0) {
0124                 debug("Provided SimCalorimeterHitCollection is empty. No truth association will be performed.");
0125                 continue;
0126             } else {
0127                 associate_mc_particles(cl, mchits, associations);
0128             }
0129 #endif
0130         }
0131 
0132         // debug output
0133         for (const auto& cl: *clusters) {
0134             debug("Cluster {:d}: Edep = {:.3f} MeV, Dir = ({:.3f}, {:.3f}) deg", cl.getObjectID().index,
0135                          cl.getEnergy() * 1000., cl.getIntrinsicTheta() / M_PI * 180.,
0136                          cl.getIntrinsicPhi() / M_PI * 180.
0137             );
0138         }
0139     }
0140 
0141   private:
0142 
0143     static std::vector<edm4eic::MutableCluster> reconstruct_cluster_layers(const edm4eic::ProtoCluster& pcl) {
0144         const auto& hits = pcl.getHits();
0145         const auto& weights = pcl.getWeights();
0146         // using map to have hits sorted by layer
0147         std::map<int, std::vector<std::pair<const edm4eic::CalorimeterHit, float>>> layer_map;
0148         for (unsigned i = 0; i < hits.size(); ++i) {
0149             const auto hit = hits[i];
0150             auto lid = hit.getLayer();
0151 //            if (layer_map.count(lid) == 0) {
0152 //                std::vector<std::pair<const edm4eic::CalorimeterHit, float>> v;
0153 //                layer_map[lid] = {};
0154 //            }
0155             layer_map[lid].push_back({hit, weights[i]});
0156         }
0157 
0158         // create layers
0159         std::vector<edm4eic::MutableCluster> cl_layers;
0160         for (const auto &[lid, layer_hits]: layer_map) {
0161             auto layer = reconstruct_layer(layer_hits);
0162             cl_layers.push_back(layer);
0163         }
0164         return cl_layers;
0165     }
0166 
0167     static edm4eic::MutableCluster reconstruct_layer(const std::vector<std::pair<const edm4eic::CalorimeterHit, float>>& hits) {
0168         edm4eic::MutableCluster layer;
0169         layer.setType(Jug::Reco::ClusterType::kClusterSlice);
0170         // Calculate averages
0171         double energy{0};
0172         double energyError{0};
0173         double time{0};
0174         double timeError{0};
0175         double sumOfWeights{0};
0176         auto pos = layer.getPosition();
0177         for (const auto &[hit, weight]: hits) {
0178             energy += hit.getEnergy() * weight;
0179             energyError += std::pow(hit.getEnergyError() * weight, 2);
0180             time += hit.getTime() * weight;
0181             timeError += std::pow(hit.getTimeError() * weight, 2);
0182             pos = pos + hit.getPosition() * weight;
0183             sumOfWeights += weight;
0184             layer.addToHits(hit);
0185         }
0186         layer.setEnergy(energy);
0187         layer.setEnergyError(std::sqrt(energyError));
0188         layer.setTime(time / sumOfWeights);
0189         layer.setTimeError(std::sqrt(timeError) / sumOfWeights);
0190         layer.setNhits(hits.size());
0191         layer.setPosition(pos / sumOfWeights);
0192         // positionError not set
0193         // Intrinsic direction meaningless in a cluster layer --> not set
0194 
0195         // Calculate radius as the standard deviation of the hits versus the cluster center
0196         double radius = 0.;
0197         for (const auto &[hit, weight]: hits) {
0198             radius += std::pow(edm4hep::utils::magnitude(hit.getPosition() - layer.getPosition()), 2);
0199         }
0200         layer.addToShapeParameters(std::sqrt(radius / layer.getNhits()));
0201         // TODO Skewedness
0202 
0203         return layer;
0204     }
0205 
0206     static edm4eic::MutableCluster reconstruct_cluster(const edm4eic::ProtoCluster& pcl) {
0207         edm4eic::MutableCluster cluster;
0208 
0209         const auto& hits = pcl.getHits();
0210         const auto& weights = pcl.getWeights();
0211 
0212         cluster.setType(Jug::Reco::ClusterType::kCluster3D);
0213         double energy = 0.;
0214         double energyError = 0.;
0215         double time = 0.;
0216         double timeError = 0.;
0217         double meta = 0.;
0218         double mphi = 0.;
0219         double r = 9999 * dd4hep::cm;
0220         for (unsigned i = 0; i < hits.size(); ++i) {
0221             const auto &hit = hits[i];
0222             const auto &weight = weights[i];
0223             energy += hit.getEnergy() * weight;
0224             energyError += std::pow(hit.getEnergyError() * weight, 2);
0225             // energy weighting for the other variables
0226             const double energyWeight = hit.getEnergy() * weight;
0227             time += hit.getTime() * energyWeight;
0228             timeError += std::pow(hit.getTimeError() * energyWeight, 2);
0229             meta += edm4hep::utils::eta(hit.getPosition()) * energyWeight;
0230             mphi += edm4hep::utils::angleAzimuthal(hit.getPosition()) * energyWeight;
0231             r = std::min(edm4hep::utils::magnitude(hit.getPosition()), r);
0232             cluster.addToHits(hit);
0233         }
0234         cluster.setEnergy(energy);
0235         cluster.setEnergyError(std::sqrt(energyError));
0236         cluster.setTime(time / energy);
0237         cluster.setTimeError(std::sqrt(timeError) / energy);
0238         cluster.setNhits(hits.size());
0239         cluster.setPosition(edm4hep::utils::sphericalToVector(r, edm4hep::utils::etaToAngle(meta / energy), mphi / energy));
0240 
0241         // shower radius estimate (eta-phi plane)
0242         double radius = 0.;
0243         for (const auto &hit: hits) {
0244             radius += std::pow(
0245               std::hypot(
0246                 (edm4hep::utils::eta(hit.getPosition()) - edm4hep::utils::eta(cluster.getPosition())),
0247                 (edm4hep::utils::angleAzimuthal(hit.getPosition()) - edm4hep::utils::angleAzimuthal(cluster.getPosition()))
0248               ),
0249               2.0
0250             );
0251         }
0252         cluster.addToShapeParameters(std::sqrt(radius / cluster.getNhits()));
0253         // Skewedness not calculated TODO
0254 
0255         // Optionally store the MC truth associated with the first hit in this cluster
0256         // FIXME no connection between cluster and truth in edm4hep
0257         // if (mcHits) {
0258         //  const auto& mc_hit    = (*mcHits)[pcl.getHits(0).ID.value];
0259         //  cluster.mcID({mc_hit.truth().trackID, m_kMonteCarloSource});
0260         //}
0261 
0262         return cluster;
0263     }
0264 
0265     std::pair<double /* polar */, double /* azimuthal */> fit_track(const std::vector<edm4eic::MutableCluster> &layers) const {
0266         int nrows = 0;
0267         decltype(edm4eic::ClusterData::position) mean_pos{0, 0, 0};
0268         for (const auto &layer: layers) {
0269             if ((layer.getNhits() > 0) && (layer.getHits(0).getLayer() <= m_cfg.trackStopLayer)) {
0270                 mean_pos = mean_pos + layer.getPosition();
0271                 nrows += 1;
0272             }
0273         }
0274 
0275         // cannot fit
0276         if (nrows < 2) {
0277             return {};
0278         }
0279 
0280         mean_pos = mean_pos / nrows;
0281         // fill position data
0282         Eigen::MatrixXd pos(nrows, 3);
0283         int ir = 0;
0284         for (const auto &layer: layers) {
0285             if ((layer.getNhits() > 0) && (layer.getHits(0).getLayer() <= m_cfg.trackStopLayer)) {
0286                 auto delta = layer.getPosition() - mean_pos;
0287                 pos(ir, 0) = delta.x;
0288                 pos(ir, 1) = delta.y;
0289                 pos(ir, 2) = delta.z;
0290                 ir += 1;
0291             }
0292         }
0293 
0294         Eigen::JacobiSVD <Eigen::MatrixXd> svd(pos, Eigen::ComputeThinU | Eigen::ComputeThinV);
0295         const auto dir = svd.matrixV().col(0);
0296         // theta and phi
0297         return {std::acos(dir(2)), std::atan2(dir(1), dir(0))};
0298     }
0299 
0300     void associate_mc_particles(
0301         const edm4eic::Cluster& cl,
0302 #if EDM4EIC_VERSION_MAJOR >= 7
0303         const edm4eic::MCRecoCalorimeterHitAssociationCollection* mchitassociations,
0304 #else
0305         const edm4hep::SimCalorimeterHitCollection* mchits,
0306 #endif
0307         edm4eic::MCRecoClusterParticleAssociationCollection* assocs
0308     ) const {
0309         // --------------------------------------------------------------------------
0310         // Association Logic
0311         // --------------------------------------------------------------------------
0312         /*  1. identify all sim hits associated with a given protocluster, and sum
0313          *     the energy of the sim hits.
0314          *  2. for each sim hit
0315          *     - identify parents of each contributing particles; and
0316          *     - if parent is a primary particle, add to list of contributors
0317          *       and sum the energy contributed by the parent.
0318          *  3. create an association for each contributing primary with a weight
0319          *     of contributed energy over total sim hit energy.
0320          */
0321 
0322         // lambda to compare MCParticles
0323         auto compare = [](const edm4hep::MCParticle& lhs, const edm4hep::MCParticle& rhs) {
0324             if (lhs.getObjectID().collectionID == rhs.getObjectID().collectionID) {
0325                 return (lhs.getObjectID().index < rhs.getObjectID().index);
0326             } else {
0327                 return (lhs.getObjectID().collectionID < rhs.getObjectID().collectionID);
0328             }
0329         };
0330 
0331         // bookkeeping maps for associated primaries
0332         std::map<edm4hep::MCParticle, double, decltype(compare)> mapMCParToContrib(compare);
0333 
0334         // --------------------------------------------------------------------------
0335         // 1. get associated sim hits and sum energy
0336         // --------------------------------------------------------------------------
0337         double eSimHitSum = 0.;
0338         for (auto clhit : cl.getHits()) {
0339             // vector to hold associated sim hits
0340             std::vector<edm4hep::SimCalorimeterHit> vecAssocSimHits;
0341 
0342 #if EDM4EIC_VERSION_MAJOR >= 7
0343             for (const auto& hitAssoc : *mchitassociations) {
0344                 // if found corresponding raw hit, add sim hit to vector
0345                 // and increment energy sum
0346                 if (clhit.getRawHit() == hitAssoc.getRawHit()) {
0347                     vecAssocSimHits.push_back(hitAssoc.getSimHit());
0348                     eSimHitSum += vecAssocSimHits.back().getEnergy();
0349                 }
0350 
0351             }
0352 #else
0353             for (const auto& mchit : *mchits) {
0354                 if (mchit.getCellID() == clhit.getCellID()) {
0355                     vecAssocSimHits.push_back(mchit);
0356                     eSimHitSum += vecAssocSimHits.back().getEnergy();
0357                     break;
0358                 }
0359             }
0360 
0361             // if no matching cell ID found, continue
0362             // otherwise increment sum
0363             if (vecAssocSimHits.empty()) {
0364                 debug("No matching SimHit for hit {}", clhit.getCellID());
0365                 continue;
0366             }
0367 #endif
0368             debug("{} associated sim hits found for reco hit (cell ID = {})", vecAssocSimHits.size(), clhit.getCellID());
0369 
0370             // ------------------------------------------------------------------------
0371             // 2. loop through associated sim hits
0372             // ------------------------------------------------------------------------
0373             for (const auto& simHit : vecAssocSimHits) {
0374                 for (const auto& contrib : simHit.getContributions()) {
0375                     // --------------------------------------------------------------------
0376                     // grab primary responsible for contribution & increment relevant sum
0377                     // --------------------------------------------------------------------
0378                     edm4hep::MCParticle primary = get_primary(contrib);
0379                     mapMCParToContrib[primary] += contrib.getEnergy();
0380 
0381                     trace("Identified primary: id = {}, pid = {}, total energy = {}, contributed = {}",
0382                         primary.getObjectID().index,
0383                         primary.getPDG(),
0384                         primary.getEnergy(),
0385                         mapMCParToContrib[primary]
0386                     );
0387                 }
0388             }
0389         }
0390         debug("Found {} primaries contributing a total of {} GeV", mapMCParToContrib.size(), eSimHitSum);
0391 
0392         // --------------------------------------------------------------------------
0393         // 3. create association for each contributing primary
0394         // --------------------------------------------------------------------------
0395         for (auto [part, contribution] : mapMCParToContrib) {
0396             // calculate weight
0397             const double weight = contribution / eSimHitSum;
0398 
0399             // set association
0400             auto assoc = assocs->create();
0401             assoc.setRecID(cl.getObjectID().index); // if not using collection, this is always set to -1
0402             assoc.setSimID(part.getObjectID().index);
0403             assoc.setWeight(weight);
0404             assoc.setRec(cl);
0405             assoc.setSim(part);
0406             debug("Associated cluster #{} to MC Particle #{} (pid = {}, status = {}, energy = {}) with weight ({})",
0407                 cl.getObjectID().index,
0408                 part.getObjectID().index,
0409                 part.getPDG(),
0410                 part.getGeneratorStatus(),
0411                 part.getEnergy(),
0412                 weight
0413             );
0414         }
0415     }
0416 
0417     edm4hep::MCParticle get_primary(const edm4hep::CaloHitContribution& contrib) const {
0418         // get contributing particle
0419         const auto contributor = contrib.getParticle();
0420 
0421         // walk back through parents to find primary
0422         //   - TODO finalize primary selection. This
0423         //     can be improved!!
0424         edm4hep::MCParticle primary = contributor;
0425         while (primary.parents_size() > 0) {
0426             if (primary.getGeneratorStatus() != 0) break;
0427             primary = primary.getParents(0);
0428          }
0429          return primary;
0430     }
0431 
0432 };
0433 
0434 } // namespace eicrecon