Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2024-09-27 07:02:56

0001 // SPDX-License-Identifier: LGPL-3.0-or-later
0002 // Copyright (C) 2022 - 2024 Sylvester Joosten, Chao, Chao Peng, Whitney Armstrong, Dhevan Gangadharan, Derek Anderson
0003 
0004 /*
0005  *  Reconstruct the cluster with Center of Gravity method
0006  *  Logarithmic weighting is used for mimicking energy deposit in transverse direction
0007  *
0008  *  Author: Chao Peng (ANL), 09/27/2020
0009  */
0010 
0011 #include <Evaluator/DD4hepUnits.h>
0012 #include <boost/algorithm/string/join.hpp>
0013 #include <boost/iterator/iterator_facade.hpp>
0014 #include <boost/range/adaptor/map.hpp>
0015 #include <edm4eic/CalorimeterHitCollection.h>
0016 #include <edm4hep/RawCalorimeterHit.h>
0017 #include <edm4hep/SimCalorimeterHitCollection.h>
0018 #include <edm4hep/Vector3f.h>
0019 #include <edm4hep/utils/vector_utils.h>
0020 #include <fmt/core.h>
0021 #include <podio/ObjectID.h>
0022 #include <podio/RelationRange.h>
0023 #include <Eigen/Core>
0024 #include <Eigen/Eigenvalues>
0025 #include <Eigen/Householder> // IWYU pragma: keep
0026 #include <cctype>
0027 #include <complex>
0028 #include <cstddef>
0029 #include <gsl/pointers>
0030 #include <iterator>
0031 #include <limits>
0032 #include <map>
0033 #include <optional>
0034 #include <vector>
0035 
0036 #include "CalorimeterClusterRecoCoG.h"
0037 #include "algorithms/calorimetry/CalorimeterClusterRecoCoGConfig.h"
0038 
0039 namespace eicrecon {
0040 
0041   using namespace dd4hep;
0042 
0043   void CalorimeterClusterRecoCoG::init() {
0044     // select weighting method
0045     std::string ew = m_cfg.energyWeight;
0046     // make it case-insensitive
0047     std::transform(ew.begin(), ew.end(), ew.begin(), [](char s) { return std::tolower(s); });
0048     auto it = weightMethods.find(ew);
0049     if (it == weightMethods.end()) {
0050       error("Cannot find energy weighting method {}, choose one from [{}]", m_cfg.energyWeight, boost::algorithm::join(weightMethods | boost::adaptors::map_keys, ", "));
0051       return;
0052     }
0053     weightFunc = it->second;
0054   }
0055 
0056   void CalorimeterClusterRecoCoG::process(
0057       const CalorimeterClusterRecoCoG::Input& input,
0058       const CalorimeterClusterRecoCoG::Output& output) const {
0059 #if EDM4EIC_VERSION_MAJOR >= 7
0060     const auto [proto, mchitassociations] = input;
0061 #else
0062     const auto [proto, mchits] = input;
0063 #endif
0064     auto [clusters, associations] = output;
0065 
0066     for (const auto& pcl : *proto) {
0067       // skip protoclusters with no hits
0068       if (pcl.hits_size() == 0) {
0069         continue;
0070       }
0071 
0072       auto cl_opt = reconstruct(pcl);
0073       if (! cl_opt.has_value()) {
0074         continue;
0075       }
0076       auto cl = *std::move(cl_opt);
0077 
0078       debug("{} hits: {} GeV, ({}, {}, {})", cl.getNhits(), cl.getEnergy() / dd4hep::GeV, cl.getPosition().x / dd4hep::mm, cl.getPosition().y / dd4hep::mm, cl.getPosition().z / dd4hep::mm);
0079       clusters->push_back(cl);
0080 
0081       // If sim hits are available, associate cluster with MCParticle
0082 #if EDM4EIC_VERSION_MAJOR >= 7
0083       if (mchitassociations->size() == 0) {
0084         debug("Provided MCRecoCalorimeterHitAssociation collection is empty. No truth associations will be performed.");
0085         continue;
0086       } else {
0087         associate(cl, mchitassociations, associations);
0088       }
0089 #else
0090       if (mchits->size() == 0) {
0091         debug("Provided SimCalorimeterHitCollection is empty. No truth association will be performed.");
0092         continue;
0093       } else {
0094         associate(cl, mchits, associations);
0095       }
0096 #endif
0097     }
0098 }
0099 
0100 std::optional<edm4eic::MutableCluster> CalorimeterClusterRecoCoG::reconstruct(const edm4eic::ProtoCluster& pcl) const {
0101   edm4eic::MutableCluster cl;
0102   cl.setNhits(pcl.hits_size());
0103 
0104   debug("hit size = {}", pcl.hits_size());
0105 
0106   // no hits
0107   if (pcl.hits_size() == 0) {
0108     return {};
0109   }
0110 
0111   // calculate total energy, find the cell with the maximum energy deposit
0112   float totalE = 0.;
0113   // Used to optionally constrain the cluster eta to those of the contributing hits
0114   float minHitEta = std::numeric_limits<float>::max();
0115   float maxHitEta = std::numeric_limits<float>::min();
0116   auto time      = 0;
0117   auto timeError = 0;
0118   for (unsigned i = 0; i < pcl.getHits().size(); ++i) {
0119     const auto& hit   = pcl.getHits()[i];
0120     const auto weight = pcl.getWeights()[i];
0121     debug("hit energy = {} hit weight: {}", hit.getEnergy(), weight);
0122     auto energy = hit.getEnergy() * weight;
0123     totalE += energy;
0124     time += (hit.getTime() - time) * energy / totalE;
0125     cl.addToHits(hit);
0126     cl.addToHitContributions(energy);
0127     const float eta = edm4hep::utils::eta(hit.getPosition());
0128     if (eta < minHitEta) {
0129       minHitEta = eta;
0130     }
0131     if (eta > maxHitEta) {
0132       maxHitEta = eta;
0133     }
0134   }
0135   cl.setEnergy(totalE / m_cfg.sampFrac);
0136   cl.setEnergyError(0.);
0137   cl.setTime(time);
0138   cl.setTimeError(timeError);
0139 
0140   // center of gravity with logarithmic weighting
0141   float tw = 0.;
0142   auto v   = cl.getPosition();
0143 
0144   double logWeightBase=m_cfg.logWeightBase;
0145   if (m_cfg.logWeightBaseCoeffs.size() != 0){
0146     double l=log(cl.getEnergy()/m_cfg.logWeightBase_Eref);
0147     logWeightBase=0;
0148     for(std::size_t i =0; i<m_cfg.logWeightBaseCoeffs.size(); i++){
0149       logWeightBase += m_cfg.logWeightBaseCoeffs[i]*pow(l,i);
0150     }
0151   }
0152 
0153   for (unsigned i = 0; i < pcl.getHits().size(); ++i) {
0154     const auto& hit   = pcl.getHits()[i];
0155     const auto weight = pcl.getWeights()[i];
0156     //      _DBG_<<" -- weight = " << weight << "  E=" << hit.getEnergy() << " totalE=" <<totalE << " log(E/totalE)=" << std::log(hit.getEnergy()/totalE) << std::endl;
0157     float w           = weightFunc(hit.getEnergy() * weight, totalE, logWeightBase, 0);
0158     tw += w;
0159     v = v + (hit.getPosition() * w);
0160   }
0161   if (tw == 0.) {
0162     warning("zero total weights encountered, you may want to adjust your weighting parameter.");
0163     return {};
0164   }
0165   cl.setPosition(v / tw);
0166   cl.setPositionError({}); // @TODO: Covariance matrix
0167 
0168   // Optionally constrain the cluster to the hit eta values
0169   if (m_cfg.enableEtaBounds) {
0170     const bool overflow  = (edm4hep::utils::eta(cl.getPosition()) > maxHitEta);
0171     const bool underflow = (edm4hep::utils::eta(cl.getPosition()) < minHitEta);
0172     if (overflow || underflow) {
0173       const double newEta   = overflow ? maxHitEta : minHitEta;
0174       const double newTheta = edm4hep::utils::etaToAngle(newEta);
0175       const double newR     = edm4hep::utils::magnitude(cl.getPosition());
0176       const double newPhi   = edm4hep::utils::angleAzimuthal(cl.getPosition());
0177       cl.setPosition(edm4hep::utils::sphericalToVector(newR, newTheta, newPhi));
0178       debug("Bound cluster position to contributing hits due to {}", (overflow ? "overflow" : "underflow"));
0179     }
0180   }
0181 
0182   // Additional convenience variables
0183 
0184   //_______________________________________
0185   // Calculate cluster profile:
0186   //    radius,
0187   //    dispersion (energy weighted radius),
0188   //    theta-phi cluster widths (2D)
0189   //    x-y-z cluster widths (3D)
0190   float radius = 0, dispersion = 0, w_sum = 0;
0191 
0192   Eigen::Matrix2f sum2_2D = Eigen::Matrix2f::Zero();
0193   Eigen::Matrix3f sum2_3D = Eigen::Matrix3f::Zero();
0194   Eigen::Vector2f sum1_2D = Eigen::Vector2f::Zero();
0195   Eigen::Vector3f sum1_3D = Eigen::Vector3f::Zero();
0196   Eigen::Vector2cf eigenValues_2D = Eigen::Vector2cf::Zero();
0197   Eigen::Vector3cf eigenValues_3D = Eigen::Vector3cf::Zero();
0198   // the axis is the direction of the eigenvalue corresponding to the largest eigenvalue.
0199   edm4hep::Vector3f axis;
0200 
0201   if (cl.getNhits() > 1) {
0202     for (const auto& hit : pcl.getHits()) {
0203       float w = weightFunc(hit.getEnergy(), totalE, logWeightBase, 0);
0204 
0205       // theta, phi
0206       Eigen::Vector2f pos2D( edm4hep::utils::anglePolar( hit.getPosition() ), edm4hep::utils::angleAzimuthal( hit.getPosition() ) );
0207       // x, y, z
0208       Eigen::Vector3f pos3D( hit.getPosition().x, hit.getPosition().y, hit.getPosition().z );
0209 
0210       const auto delta = cl.getPosition() - hit.getPosition();
0211       radius          += delta * delta;
0212       dispersion      += delta * delta * w;
0213 
0214       // Weighted Sum x*x, x*y, x*z, y*y, etc.
0215       sum2_2D += w * pos2D * pos2D.transpose();
0216       sum2_3D += w * pos3D * pos3D.transpose();
0217 
0218       // Weighted Sum x, y, z
0219       sum1_2D += w * pos2D;
0220       sum1_3D += w * pos3D;
0221 
0222       w_sum += w;
0223     }
0224 
0225     radius     = sqrt((1. / (cl.getNhits() - 1.)) * radius);
0226     if( w_sum > 0 ) {
0227       dispersion = sqrt( dispersion / w_sum );
0228 
0229       // normalize matrices
0230       sum2_2D /= w_sum;
0231       sum2_3D /= w_sum;
0232       sum1_2D /= w_sum;
0233       sum1_3D /= w_sum;
0234 
0235       // 2D and 3D covariance matrices
0236       Eigen::Matrix2f cov2 = sum2_2D - sum1_2D * sum1_2D.transpose();
0237       Eigen::Matrix3f cov3 = sum2_3D - sum1_3D * sum1_3D.transpose();
0238 
0239       // Solve for eigenvalues.  Corresponds to cluster's 2nd moments (widths)
0240       Eigen::EigenSolver<Eigen::Matrix2f> es_2D(cov2, false); // set to true for eigenvector calculation
0241       Eigen::EigenSolver<Eigen::Matrix3f> es_3D(cov3, true); // set to true for eigenvector calculation
0242 
0243       // eigenvalues of symmetric real matrix are always real
0244       eigenValues_2D = es_2D.eigenvalues();
0245       eigenValues_3D = es_3D.eigenvalues();
0246       //find the eigenvector corresponding to the largest eigenvalue
0247       auto eigenvectors= es_3D.eigenvectors();
0248       auto max_eigenvalue_it = std::max_element(
0249         eigenValues_3D.begin(),
0250         eigenValues_3D.end(),
0251         [](auto a, auto b) {
0252             return std::real(a) < std::real(b);
0253         }
0254       );
0255       auto axis_eigen = eigenvectors.col(std::distance(
0256             eigenValues_3D.begin(),
0257             max_eigenvalue_it
0258         ));
0259       axis = {
0260         axis_eigen(0,0).real(),
0261         axis_eigen(1,0).real(),
0262         axis_eigen(2,0).real(),
0263       };
0264     }
0265   }
0266 
0267   cl.addToShapeParameters( radius );
0268   cl.addToShapeParameters( dispersion );
0269   cl.addToShapeParameters( eigenValues_2D[0].real() ); // 2D theta-phi cluster width 1
0270   cl.addToShapeParameters( eigenValues_2D[1].real() ); // 2D theta-phi cluster width 2
0271   cl.addToShapeParameters( eigenValues_3D[0].real() ); // 3D x-y-z cluster width 1
0272   cl.addToShapeParameters( eigenValues_3D[1].real() ); // 3D x-y-z cluster width 2
0273   cl.addToShapeParameters( eigenValues_3D[2].real() ); // 3D x-y-z cluster width 3
0274 
0275 
0276   double dot_product = cl.getPosition() * axis;
0277   if (dot_product < 0) {
0278     axis = -1 * axis;
0279   }
0280 
0281   if (m_cfg.longitudinalShowerInfoAvailable) {
0282     cl.setIntrinsicTheta(edm4hep::utils::anglePolar(axis));
0283     cl.setIntrinsicPhi(edm4hep::utils::angleAzimuthal(axis));
0284     // TODO intrinsicDirectionError
0285   } else {
0286     cl.setIntrinsicTheta(NAN);
0287     cl.setIntrinsicPhi(NAN);
0288   }
0289 
0290   return std::move(cl);
0291 }
0292 
0293 void CalorimeterClusterRecoCoG::associate(
0294   const edm4eic::Cluster& cl,
0295 #if EDM4EIC_VERSION_MAJOR >= 7
0296   const edm4eic::MCRecoCalorimeterHitAssociationCollection* mchitassociations,
0297 #else
0298   const edm4hep::SimCalorimeterHitCollection* mchits,
0299 #endif
0300   edm4eic::MCRecoClusterParticleAssociationCollection* assocs
0301 ) const {
0302   // --------------------------------------------------------------------------
0303   // Association Logic
0304   // --------------------------------------------------------------------------
0305   /*  1. identify all sim hits associated with a given protocluster, and sum
0306    *     the energy of the sim hits.
0307    *  2. for each sim hit
0308    *     - identify parents of each contributing particles; and
0309    *     - if parent is a primary particle, add to list of contributors
0310    *       and sum the energy contributed by the parent.
0311    *  3. create an association for each contributing primary with a weight
0312    *     of contributed energy over total sim hit energy.
0313    */
0314 
0315   // lambda to compare MCParticles
0316   auto compare = [](const edm4hep::MCParticle& lhs, const edm4hep::MCParticle& rhs) {
0317     if (lhs.getObjectID().collectionID == rhs.getObjectID().collectionID) {
0318       return (lhs.getObjectID().index < rhs.getObjectID().index);
0319     } else {
0320       return (lhs.getObjectID().collectionID < rhs.getObjectID().collectionID);
0321     }
0322   };
0323 
0324   // bookkeeping maps for associated primaries
0325   std::map<edm4hep::MCParticle, double, decltype(compare)> mapMCParToContrib(compare);
0326 
0327   // --------------------------------------------------------------------------
0328   // 1. get associated sim hits and sum energy
0329   // --------------------------------------------------------------------------
0330   double eSimHitSum = 0.;
0331   for (auto clhit : cl.getHits()) {
0332     // vector to hold associated sim hits
0333     std::vector<edm4hep::SimCalorimeterHit> vecAssocSimHits;
0334 
0335 #if EDM4EIC_VERSION_MAJOR >= 7
0336     for (const auto& hitAssoc : *mchitassociations) {
0337       // if found corresponding raw hit, add sim hit to vector
0338       // and increment energy sum
0339       if (clhit.getRawHit() == hitAssoc.getRawHit()) {
0340         vecAssocSimHits.push_back(hitAssoc.getSimHit());
0341         eSimHitSum += vecAssocSimHits.back().getEnergy();
0342       }
0343 
0344     }
0345 #else
0346     for (const auto& mchit : *mchits) {
0347       if (mchit.getCellID() == clhit.getCellID()) {
0348          vecAssocSimHits.push_back(mchit);
0349         break;
0350       }
0351     }
0352 
0353     // if no matching cell ID found, continue
0354     // otherwise increment sum
0355     if (vecAssocSimHits.empty()) {
0356       debug("No matching SimHit for hit {}", clhit.getCellID());
0357       continue;
0358     } else {
0359       eSimHitSum += vecAssocSimHits.back().getEnergy();
0360     }
0361 #endif
0362     debug("{} associated sim hits found for reco hit (cell ID = {})", vecAssocSimHits.size(), clhit.getCellID());
0363 
0364     // ------------------------------------------------------------------------
0365     // 2. loop through associated sim hits
0366     // ------------------------------------------------------------------------
0367     for (const auto& simHit : vecAssocSimHits) {
0368       for (const auto& contrib : simHit.getContributions()) {
0369         // --------------------------------------------------------------------
0370         // grab primary responsible for contribution & increment relevant sum
0371         // --------------------------------------------------------------------
0372         edm4hep::MCParticle primary = get_primary(contrib);
0373         mapMCParToContrib[primary] += contrib.getEnergy();
0374 
0375         trace("Identified primary: id = {}, pid = {}, total energy = {}, contributed = {}",
0376           primary.getObjectID().index,
0377           primary.getPDG(),
0378           primary.getEnergy(),
0379           mapMCParToContrib[primary]
0380         );
0381       }
0382     }
0383   }
0384   debug("Found {} primaries contributing a total of {} GeV", mapMCParToContrib.size(), eSimHitSum);
0385 
0386   // --------------------------------------------------------------------------
0387   // 3. create association for each contributing primary
0388   // --------------------------------------------------------------------------
0389   for (auto [part, contribution] : mapMCParToContrib) {
0390     // calculate weight
0391     const double weight = contribution / eSimHitSum;
0392 
0393     // set association
0394     auto assoc = assocs->create();
0395     assoc.setRecID(cl.getObjectID().index); // if not using collection, this is always set to -1
0396     assoc.setSimID(part.getObjectID().index);
0397     assoc.setWeight(weight);
0398     assoc.setRec(cl);
0399     assoc.setSim(part);
0400     debug("Associated cluster #{} to MC Particle #{} (pid = {}, status = {}, energy = {}) with weight ({})",
0401       cl.getObjectID().index,
0402       part.getObjectID().index,
0403       part.getPDG(),
0404       part.getGeneratorStatus(),
0405       part.getEnergy(),
0406       weight
0407     );
0408   }
0409 }
0410 
0411 edm4hep::MCParticle CalorimeterClusterRecoCoG::get_primary(const edm4hep::CaloHitContribution& contrib) const {
0412   // get contributing particle
0413   const auto contributor = contrib.getParticle();
0414 
0415   // walk back through parents to find primary
0416   //   - TODO finalize primary selection. This
0417   //     can be improved!!
0418   edm4hep::MCParticle primary = contributor;
0419   while (primary.parents_size() > 0) {
0420     if (primary.getGeneratorStatus() != 0) break;
0421     primary = primary.getParents(0);
0422   }
0423   return primary;
0424 }
0425 
0426 }