Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-04-08 07:51:18

0001 // SPDX-License-Identifier: LGPL-3.0-or-later
0002 // Copyright (C) 2022 - 2025 Whitney Armstrong, Wouter Deconinck, Sylvester Joosten, Dmitry Romanov, Yann Bedfer
0003 
0004 /*
0005   Digitization specific to MPGDs.
0006   - What's special in MPGDs is their 2D-strip readout: i.e. simultaneous
0007    registering along two sets of coordinate strips.
0008   - The fact imposes strong constraints on digitization, stemming from DD4hep's
0009    provision that one and only one readout surface be associated to a sensitive
0010    volume. The one-and-only-one rule is enforced through to ACTS's TrackState,
0011    forbidding simple solutions exploiting MultiSegmentation to create two strip
0012    hits on a same surface.
0013   - Present solution is based on multiple sensitive SUBVOLUMES. FIVE of them:
0014     + TWO persistent ones, acting as READOUT SURFACES (i.e. to which raw hits
0015      are assigned, in both RealData and MC): very thin and sitting very close
0016      to mid-plane,
0017     + THREE HELPER ones: TWO RADIATORS, thick, serving temporarily to collect
0018      energy deposit and ONE REFERENCE, thin and sitting exactly at mid-plane.
0019      (The REFERENCE is not essential, but found to help rationalize the source
0020      code.)
0021   - Each SUBVOLUME has a distinct ID, w/ a distinctive "STRIP" FIELD, but all
0022    share a common XML <readout> thanks to a MultiSegmentation based on the
0023    "STRIP" FIELD.
0024   - Several subHits are created (up to five, one per individual SUBVOLUME)
0025    corresponding to a SINGLE I[ONISATION]+A[MPLIFICATION] process, inducing in
0026    turn CORRELATED signals on two coordinates.
0027   - These subHits are EXTENDED so as to span the full sensitive volume and be
0028    on the sensitive surface of the REFERENCE SUBVOLUME (its mid-plane), and
0029    hence reconstitute the proper hit position of the TRAVERSING particle.
0030     This, for particles that are determined to be indeed traversing.
0031   - We then want to preserve the I+A correlation when accumulating hits on the
0032    readout channels. This could be obtained by accumulating directly subHits (
0033    as is done in "SiliconTrackerDigi", as of commit #151496af). For such a
0034    scheme to preserve CORRELATION, we would have to collect all related subHits
0035    (i.e. those deemed to originate from a given I+A) and apply to them a common
0036    smearing (in amplitude and time). Here, we go one step further and COALESCE
0037    the related subHits. Our guideline can be formulated as: reproduce what we
0038    would get would there be a single volume. This comes with a cost of extra
0039    complication in the source code, but a limited one. This should go on w/
0040    the common smearing simulating I+A, independent smearings simulating CHARGE
0041    SHARING and CHARGE SPREADING. Then accumulation channel by channel. Then,
0042    possibly, apply smearing simulating FE electronics.
0043   - In any case, we therefore have a DOUBLE LOOP ON INPUT SimTrackerHits, to
0044    collect those subHits arising from the same I+A. I.e. SAME P[ARTICLE],
0045    M[ODULE] (a particle can fire two overlapping modules, not to be mixed) and
0046    O[RIGIN] (direct or via secondary).
0047   - The double loop is optimized for speed (by keeping track of the start index
0048    of the second loop) in order to not waste time on high multiplicity events.
0049   - We assume subHits originating from the same ionization process to come in
0050    sequences unbroken by any intertwining.
0051   - A number of checks are conducted before undertaking subHit COALESCENCE:
0052     I) SubHits should be TRAVERSING, i.e. exiting and then entering through
0053     opposite walls, as opposed to exiting or entering through the edge or
0054     dying/being-born within their SUBVOLUME, see methods "(c|b)Traversing".
0055     II) SubHits have SAME PMO, see "samePMO".
0056     III) SubHits extrapolate to a common point, see "outInDistance".
0057     All these checks are done to be on the safe side. A subset of them, hinging
0058    on (III), should be sufficient. But there are so many cases to take into
0059    account that it's difficult to settle on a minimal subset.
0060   - Evaluation:
0061     + MIPs are found to be properly handled, whether a cutoff on deposited
0062      energy is applied or not: hits are COALESCED when needed. They are
0063      assigned to mid-plane, except for few cases, see "flagUnexpected".
0064     + For lower energy particles, there are at times ambiguous cases where
0065      seemingly related subHits turn out to not be COALESCED, because they fail
0066      to pass above-mentioned checks due to their erratic trajectories.
0067       Note that even if they are genuinely related, this is not dramatic:
0068      subHits will still be directly accumulated on their common (within pitch
0069      precision) cell, only that we miss the correlation.
0070     + Special cases of i) particle exiting and reEntering the same SUBVOLUME
0071      in the cylindrical setup, ii) SimHit positioned outside the SUBVOLUME (
0072      cylindrical setup once again) it is assigned to, are catered for. In case
0073      (i), subHits are COALESCED but not EXTENDED.
0074   - Imperfections:
0075     + In order to validate (III), one has to decide on a TOLERANCE. The ideal
0076      would be to do this based on multiscattering. Instead, what's used are
0077      guesstimates, and a recipe to RELAX TOLERANCE for low energy stuff.
0078   - Future developments:
0079    In addition to DIGITIZATION proper, the method involves SIMULATION and
0080    (re)SEGMENTATION.
0081     + SIMULATION will eventually involve simulating the amplitude and timing
0082      correlation of the two coordinates, and the spreading of the charge on
0083      adjacent strips producing multi-hit clusters. But present version is
0084      preliminary: single-hit clusters, with identical timing and same
0085      amplitude.
0086     + DIGITIZATION follows the standard steps. Remains to agree on the handling
0087      of the discrimination threshold, see Issue #1722.
0088  */
0089 
0090 #include "MPGDTrackerDigi.h"
0091 
0092 #include <DD4hep/Alignments.h>
0093 #include <DD4hep/DetElement.h>
0094 #include <DD4hep/IDDescriptor.h>
0095 #include <DD4hep/Objects.h>
0096 #include <DD4hep/Readout.h>
0097 #include <DD4hep/Shapes.h>
0098 #include <DD4hep/VolumeManager.h>
0099 #include <DD4hep/detail/SegmentationsInterna.h>
0100 #include <DDSegmentation/BitFieldCoder.h>
0101 #include <DDSegmentation/MultiSegmentation.h>
0102 #include <DDSegmentation/Segmentation.h>
0103 #include <Evaluator/DD4hepUnits.h>
0104 #include <Math/GenVector/Cartesian3D.h>
0105 #include <Math/GenVector/DisplacementVector3D.h>
0106 #include <Parsers/Primitives.h>
0107 #include <TGeoMatrix.h>
0108 #include <TMath.h>
0109 // Access "algorithms:GeoSvc"
0110 #include <algorithms/geo.h>
0111 #include <algorithms/logger.h>
0112 #include <edm4eic/unit_system.h>
0113 #include <edm4hep/EDM4hepVersion.h>
0114 #include <edm4hep/MCParticleCollection.h>
0115 #include <edm4hep/Vector3d.h>
0116 #include <edm4hep/Vector3f.h>
0117 #include <fmt/format.h>
0118 #include <podio/detail/Link.h>
0119 #include <podio/detail/LinkCollectionImpl.h>
0120 #include <algorithm>
0121 #include <cmath>
0122 #include <cstdint>
0123 #include <cstring>
0124 #include <gsl/pointers>
0125 #include <initializer_list>
0126 #include <iterator>
0127 #include <map>
0128 #include <memory>
0129 #include <random>
0130 #include <stdexcept>
0131 #include <tuple>
0132 #include <unordered_map>
0133 #include <utility>
0134 #include <vector>
0135 
0136 #include "algorithms/digi/MPGDTrackerDigiConfig.h"
0137 
0138 using namespace dd4hep;
0139 
0140 namespace eicrecon {
0141 
0142 void MPGDTrackerDigi::init() {
0143   // Access id decoder
0144   m_detector = algorithms::GeoSvc::instance().detector();
0145 
0146   if (m_cfg.readout.empty()) {
0147     throw std::runtime_error("Readout is empty");
0148   }
0149   try {
0150     m_seg    = m_detector->readout(m_cfg.readout).segmentation();
0151     m_id_dec = m_detector->readout(m_cfg.readout).idSpec().decoder();
0152   } catch (const std::runtime_error&) {
0153     critical(R"(Failed to load ID decoder for "{}" readout.)", m_cfg.readout);
0154     throw std::runtime_error("Failed to load ID decoder");
0155   }
0156 
0157   // IDDescriptor and Segmentation
0158   parseIDDescriptor();
0159   parseSegmentation();
0160 
0161   // Ordering of SUBVOLUMES (based on "STRIP" FIELD)
0162   m_stripRank = [&](CellID vID) {
0163     CellID sID = vID & m_stripBits;
0164     for (int rank = 0; rank < 5; rank++) {
0165       if (sID == m_stripIDs[rank]) {
0166         return rank;
0167       }
0168     }
0169     return -1;
0170   };
0171   m_orientation = [&](CellID vID, CellID vJD) {
0172     int ranki = m_stripRank(vID);
0173     int rankj = m_stripRank(vJD);
0174     if (rankj > ranki) {
0175       return +1;
0176     }
0177     if (rankj < ranki) {
0178       return -1;
0179     }
0180     return 0;
0181   };
0182   m_isUpstream = [](int orientation, unsigned int status) {
0183     // Outgoing particle exits...
0184     bool isUpstream =
0185         (orientation < 0 && (status & 0x2)) ||           // ...lower wall
0186         (orientation > 0 && (status & 0x8)) ||           // ...upper wall
0187         (orientation == 0 && (status & 0x102) == 0x102); // ...lower wall and can reEnter
0188     return isUpstream;
0189   };
0190   m_isDownstream = [](int orientation, unsigned int status) {
0191     // Incoming particle enters...
0192     bool isDownstream =
0193         (orientation > 0 && (status & 0x1)) ||           // ...lower wall
0194         (orientation < 0 && (status & 0x4)) ||           // ...upper wall
0195         (orientation == 0 && (status & 0x101) == 0x101); // ...lower wall and can be reEntering
0196     return isDownstream;
0197   };
0198   // RELAXED TOLERANCE
0199   m_toleranceFactor = [](double P) {
0200     int factor = 0;
0201     if (P < 1 * dd4hep::MeV) {
0202       factor = 4;
0203     } else if (P < 10 * dd4hep::MeV) {
0204       factor = 2;
0205     } else {
0206       factor = 1;
0207     }
0208     return factor;
0209   };
0210 }
0211 
0212 // Interfaces
0213 void getLocalPosMom(const edm4hep::SimTrackerHit& sim_hit, const TGeoHMatrix& toModule,
0214                     double* lpos, double* lmom);
0215 bool cExtrapolate(const double* lpos, const double* lmom, // Input subHit
0216                   double rT,                              // Target radius
0217                   double* lext);                          // Extrapolated position @ <rT>
0218 double getRef2Cur(DetElement refVol, DetElement curVol);
0219 bool bExtrapolate(const double* lpos, const double* lmom, // Input subHit
0220                   double zT,                              // Target Z
0221                   double* lext);                          // Extrapolated position @ <zT>
0222 std::string inconsistency(const edm4hep::EventHeader& event, unsigned int status, CellID cID,
0223                           const double* lpos, const double* lmom);
0224 std::string oddity(const edm4hep::EventHeader& event, unsigned int status, double dist, CellID cID,
0225                    const double* lpos, const double* lmom, CellID cJD, const double* lpoj,
0226                    const double* lmoj);
0227 double outInDistance(int shape, int orientation, double lintos[][3], double louts[][3],
0228                      double* lmom, double* lmoj);
0229 void flagUnexpected(const edm4hep::EventHeader& event, int shape, double expected,
0230                     const edm4hep::SimTrackerHit& sim_hit, double* lpini, double* lpend,
0231                     double* lpos, double* lmom);
0232 
0233 void MPGDTrackerDigi::process(const MPGDTrackerDigi::Input& input,
0234                               const MPGDTrackerDigi::Output& output) const {
0235 
0236   const auto [headers, sim_hits] = input;
0237 #if EDM4EIC_BUILD_VERSION >= EDM4EIC_VERSION(8, 7, 0)
0238   auto [raw_hits, links, associations] = output;
0239 #else
0240   auto [raw_hits, associations] = output;
0241 #endif
0242 
0243   // local random generator
0244   auto seed = m_uid.getUniqueID(*headers, name());
0245   std::default_random_engine generator(seed);
0246   std::normal_distribution<double> gaussian;
0247 
0248   // A map of unique cellIDs with temporary structure RawHit
0249   std::unordered_map<std::uint64_t, edm4eic::MutableRawTrackerHit> cell_hit_map;
0250   // A map of strip cellIDs with vector of contributing cellIDs
0251   std::map<std::uint64_t, std::vector<std::uint64_t>> stripID2cIDs;
0252   // Prepare for strip segmentation
0253   const Position dummy(0, 0, 0);
0254   const VolumeManager& volman = m_detector->volumeManager();
0255 
0256   // Reference to event, to be used to document error messages
0257   // (N.B.: I don't know how to properly handle these "headers": may there
0258   // be more than one? none?...)
0259   const edm4hep::EventHeader& header = headers->at(0);
0260 
0261   // *************** LOOP ON sim_hits
0262   size_t sim_size = sim_hits->size();
0263   for (int idx = 0; idx < (int)sim_size; idx++) {
0264     const edm4hep::SimTrackerHit& sim_hit = sim_hits->at(idx);
0265 
0266     // ***** TIME SMEARING
0267     // - Simplistic treatment.
0268     // - A more realistic one would have to distinguish a smearing common to
0269     //  both coordinates of the 2D-strip readout (mainly due to the drifting of
0270     //  the leading primary electrons of the I+A process) from other smearing
0271     //  effects, specific to each coordinate.
0272     double time_smearing = gaussian(generator) * m_cfg.timeResolution;
0273 
0274     // ***** REFERENCE SUBVOLUME
0275     CellID refID      = sim_hit.getCellID() & m_moduleBits;
0276     DetElement refVol = volman.lookupDetElement(refID);
0277     // ***** COALESCE ALL MUTUALLY CONSISTENT SUBHITS
0278     //       EXTEND TRAVERSING SUBHITS
0279     // - Needed because we want to preserve the correlation between 'p' and
0280     //  'n' strip hits resulting from a given I+A process (which is lost when
0281     //  one accumulates hits independently based on cellID).
0282     double lpos[3], eDep, time;
0283     std::vector<std::uint64_t> cIDs;
0284     const auto& shape = refVol.solid();
0285     if (std::string_view{shape.type()} == "TGeoTubeSeg") {
0286       // ********** TUBE GEOMETRY
0287       if (!cCoalesceExtend(input, idx, cIDs, lpos, eDep, time))
0288         continue;
0289     } else if (std::string_view{shape.type()} == "TGeoBBox") {
0290       // ********** BOX GEOMETRY
0291       if (!bCoalesceExtend(input, idx, cIDs, lpos, eDep, time))
0292         continue;
0293     } else {
0294       critical(R"(Bad input data: CellID {:x} has invalid shape "{}")", refID, shape.type());
0295       throw std::runtime_error(R"(Inconsistency: Inappropriate SimHits fed to "MPGDTrackerDigi".)");
0296     }
0297     // ***** CELLIDS of (p|n)-STRIP HITS
0298     Position locPos(lpos[0], lpos[1], lpos[2]); // Simplification: strip surface = REFERENCE surface
0299     // p "strip"
0300     CellID vIDp = refID | m_pStripBit;
0301     CellID cIDp = m_seg->cellID(locPos, dummy, vIDp);
0302     // n "strip"
0303     CellID vIDn         = refID | m_nStripBit;
0304     CellID cIDn         = m_seg->cellID(locPos, dummy, vIDn);
0305     double result_time  = time + time_smearing;
0306     auto hit_time_stamp = (std::int32_t)(result_time * 1e3);
0307 
0308     // ***** DEBUGGING INFO
0309     if (level() >= algorithms::LogLevel::kDebug) {
0310       for (CellID cID : {cIDp, cIDn}) {
0311         std::string sCellID = cID == cIDp ? "cellIDp" : "cellIDn";
0312         CellID hID = cID >> 32, vID = cID & m_volumeBits;
0313         debug("Hit {} = 0x{:08x}, 0x{:08x}", sCellID, hID, vID);
0314         Position stripPos = m_seg->position(cID);
0315         Position globPos  = refVol.nominal().localToWorld(stripPos);
0316         debug("  position  = ({:7.2f},{:7.2f},{:7.2f}) [mm]", globPos.X() / mm, globPos.Y() / mm,
0317               globPos.Z() / mm);
0318       }
0319       debug("  edep = {:.0f} [eV]", eDep / eV);
0320       debug("  time = {:.2f} [ns]", time);
0321 #if EDM4HEP_BUILD_VERSION >= EDM4HEP_VERSION(0, 99, 0)
0322       debug("  particle time = {} [ns]", sim_hit.getParticle().getTime());
0323 #else
0324       debug("  particle time = {} [ns]", sim_hit.getMCParticle().getTime());
0325 #endif
0326       debug("  time smearing: {:.2f}, resulting time = {:.2f} [ns]", time_smearing, result_time);
0327       debug("  hit_time_stamp: {} [~ps]", hit_time_stamp);
0328     }
0329 
0330     // ***** APPLY THRESHOLD
0331     if (eDep < m_cfg.threshold) {
0332       debug("  edep is below threshold of {:.2f} [keV]", m_cfg.threshold / keV);
0333       continue;
0334     }
0335 
0336     // ***** HIT ACCUMULATION
0337     for (CellID cID : {cIDp, cIDn}) {
0338       stripID2cIDs[cID] = cIDs;
0339       if (!cell_hit_map.contains(cID)) {
0340         // This cell doesn't have hits
0341         cell_hit_map[cID] = {
0342             cID, (std::int32_t)std::llround(eDep * 1e6),
0343             hit_time_stamp // ns->ps
0344         };
0345       } else {
0346         // There is previous values in the cell
0347         auto& hit = cell_hit_map[cID];
0348         debug("  Hit already exists in cell ID={}, prev. hit time: {}", cID, hit.getTimeStamp());
0349 
0350         // keep earliest time for hit
0351         hit.setTimeStamp(std::min(hit_time_stamp, hit.getTimeStamp()));
0352 
0353         // sum deposited energy
0354         auto charge = hit.getCharge();
0355         // Accumulate charge: shouldn't it be 'float' instead of 'int'?
0356         hit.setCharge(charge + (std::int32_t)std::llround(eDep * 1e6));
0357       }
0358     } // End loop on strip = p,n
0359   } // End loop on sim_hit's
0360 
0361   // ***** RawHit INSTANTIATION AND RawHit<-SimHits ASSOCIATION:
0362   for (auto item : cell_hit_map) {
0363     raw_hits->push_back(item.second);
0364     CellID stripID = item.first;
0365     const auto is  = stripID2cIDs.find(stripID);
0366     if (is == stripID2cIDs.end()) {
0367       error(R"(Inconsistency: CellID {:x} not found in "stripID2cIDs" map)", stripID);
0368       throw std::runtime_error(R"(Inconsistency in the handling of "stripID2cIDs" map)");
0369     }
0370     std::vector<std::uint64_t> cIDs = is->second;
0371     for (CellID cID : cIDs) {
0372       for (const auto& sim_hit : *sim_hits) {
0373         if (sim_hit.getCellID() == cID) {
0374 #if EDM4EIC_BUILD_VERSION >= EDM4EIC_VERSION(8, 7, 0)
0375           // create link
0376           auto link = links->create();
0377           link.setFrom(item.second);
0378           link.setTo(sim_hit);
0379           link.setWeight(1.0);
0380 #endif
0381           // set association
0382           auto hitassoc = associations->create();
0383           hitassoc.setWeight(1.0);
0384           hitassoc.setRawHit(item.second);
0385           hitassoc.setSimHit(sim_hit);
0386         }
0387       }
0388     }
0389   }
0390 }
0391 
0392 void MPGDTrackerDigi::parseIDDescriptor() {
0393   // Parse IDDescriptor: Retrieve CellIDs of relevant fields.
0394   // (As an illustration, here is the IDDescriptor of CyMBaL (as of 2025/11):
0395   // <id>system:8,layer:4,module:12,sensor:2,strip:28:4,phi:-16,z:-16</id>.)
0396 
0397   // - "m_volumeBits" (Volume = CellID excluding channel specification)
0398   // - "m_stripBits".
0399   debug(R"(Parsing IDDescriptor for "{}" readout)", m_cfg.readout);
0400   for (int field = 0; field < 5; field++) {
0401     const char* fieldName = m_fieldNames[field];
0402     CellID fieldID        = 0;
0403     try {
0404       fieldID = m_id_dec->get(~((CellID)0x0), fieldName);
0405     } catch (const std::runtime_error& error) {
0406       critical(R"(No field "{}" in IDDescriptor of readout "{}".)", fieldName, m_cfg.readout);
0407       throw std::runtime_error("Invalid IDDescriptor");
0408     }
0409     const BitFieldElement& fieldElement = (*m_id_dec)[fieldName];
0410     CellID fieldBits                    = fieldID << fieldElement.offset();
0411     m_volumeBits |= fieldBits;
0412     // - "m_stripBits".
0413     if (std::string_view{fieldName} == "strip") {
0414       m_stripBits = fieldBits;
0415       // SUBVOLUMES are assigned specific bits by convention
0416       CellID bits[5] = {0x3, 0x1, 0, 0x2, 0x4};
0417       for (int subVolume = 0; subVolume < 5; subVolume++) {
0418         m_stripIDs[subVolume] = bits[subVolume] << fieldElement.offset();
0419       }
0420     }
0421   }
0422   // CellIDs derived from above
0423   m_moduleBits = m_volumeBits & ~m_stripBits;
0424   m_pStripBit  = m_stripIDs[1];
0425   m_nStripBit  = m_stripIDs[3];
0426 }
0427 void MPGDTrackerDigi::parseSegmentation() {
0428   //  MPGDTrackerDigi relies on a number of assumptions concerning the
0429   // structure of the MPGD detector and its subdivision into SUBVOLUMES. These
0430   // assumptions imply in turn a particular segmentation scheme. In particular,
0431   // a MultiSegmentation allowing to navigate among the SUBVOLUMES.
0432   //  The MultiSegmentation has a discriminator named "strip", see IDDescriptor.
0433   //  It may itself be embedded in a super MultiSegmentation: case of CyMBaL.
0434   //  Let's check (limiting ourselves to main features..).
0435   debug(R"(Find valid "MultiSegmentation" for "{}" readout.)", m_cfg.readout);
0436   bool ok                          = false;
0437   using Segmentation               = dd4hep::DDSegmentation::Segmentation;
0438   const Segmentation* segmentation = m_seg->segmentation;
0439   if (segmentation->type() == "MultiSegmentation") {
0440     using MultiSegmentation = dd4hep::DDSegmentation::MultiSegmentation;
0441     const auto* multiSeg    = dynamic_cast<const MultiSegmentation*>(segmentation);
0442     if (multiSeg->discriminatorName() == "strip")
0443       ok = true;
0444     if (!ok) {
0445       for (const auto entry : multiSeg->subSegmentations()) {
0446         const Segmentation* subSegmentation = entry.segmentation;
0447         if (subSegmentation->type() == "MultiSegmentation") {
0448           const auto* subMultiSeg = dynamic_cast<const MultiSegmentation*>(subSegmentation);
0449           if (subMultiSeg->discriminatorName() == "strip") {
0450             ok = true;
0451           } else {
0452             ok = false;
0453             break;
0454           }
0455         }
0456       }
0457     }
0458   }
0459   if (!ok) {
0460     critical(
0461         R"(Segmentation for readout "{}" is not, or is not embedding, a MultiSegmentation discriminating on a "strip" field.)",
0462         m_cfg.readout.c_str());
0463     throw std::runtime_error("Invalid Segmentation");
0464   }
0465 }
0466 
0467 // ***** COALESCE subHits with same PMO
0468 //       EXTEND hits (subHits or coalesced hits) to full sensitive volume
0469 // - Input = Elementary subHit, specified as index into collection of SimHits.
0470 // - Output = Coalesced/extended hit, specified by:
0471 //  + list of cellIDs of elementary subHits contributing,
0472 //  + local position,
0473 //  + EDep,
0474 //  + time.
0475 //   Given that all segmentation classes foreseen for MPGDs ("CartesianGrid.."
0476 //  for Outer and EndCaps, "CylindricalGridPhiZ" for "CyMBaL") disregard the
0477 //  _global_ position argument to "dd4hep::Segmentation::cellID", we need
0478 //  the _local_ position and only that.
0479 // - Also returned: updated index.
0480 bool MPGDTrackerDigi::cCoalesceExtend(const Input& input, int& idx,
0481                                       std::vector<std::uint64_t>& cIDs, double* lpos, double& eDep,
0482                                       double& time) const {
0483   const auto [headers, sim_hits]        = input;
0484   const edm4hep::EventHeader& header    = headers->at(0);
0485   const edm4hep::SimTrackerHit& sim_hit = sim_hits->at(idx);
0486   CellID vID                            = sim_hit.getCellID() & m_volumeBits;
0487   CellID refID                          = vID & m_moduleBits; // => The REFERENCE SUBVOLUME
0488   const VolumeManager& volman           = m_detector->volumeManager();
0489   DetElement refVol                     = volman.lookupDetElement(refID);
0490   // TGeoHMatrix: In order to avoid a "dangling-reference" warning, let's take
0491   // a copy of the matrix instead of a reference to it.
0492   const TGeoHMatrix toRefVol = refVol.nominal().worldTransformation();
0493   double lmom[3];
0494   getLocalPosMom(sim_hit, toRefVol, lpos, lmom);
0495   const double edmm = edm4eic::unit::mm, ed2dd = dd4hep::mm / edmm;
0496   using dd4hep::mm;
0497   using edm4eic::unit::eV, edm4eic::unit::GeV;
0498   // Hit in progress
0499   eDep = sim_hit.getEDep();
0500   time = sim_hit.getTime();
0501   // Get VOLUME parameters
0502   const Tube& tRef = refVol.solid();
0503   double dZ        = tRef.dZ();
0504   // phi?
0505   // In "https://root.cern.ch/root/html534/guides/users-guide/Geometry.html"
0506   // TGeoTubeSeg: "phi1 is converted to [0,360] (but still expressed in
0507   // radian, as far as I can tell) and phi2 > phi1."
0508   // => Convert it to [-pi,+pi].
0509   double startPhi = tRef.startPhi() * radian;
0510   startPhi -= 2 * TMath::Pi();
0511   double endPhi = tRef.endPhi() * radian;
0512   endPhi -= 2 * TMath::Pi();
0513   // Get current SUBVOLUME
0514   DetElement curVol = volman.lookupDetElement(vID);
0515   const Tube& tCur  = curVol.solid();
0516   double rMin = tCur.rMin(), rMax = tCur.rMax();
0517   // Is TRAVERSING?
0518   double lintos[2][3], louts[2][3], lpini[3], lpend[3], lmend[3];
0519   std::copy(std::begin(lmom), std::end(lmom), std::begin(lmend));
0520   unsigned int status =
0521       cTraversing(lpos, lmom, sim_hit.getPathLength() * ed2dd, sim_hit.isProducedBySecondary(),
0522                   rMin, rMax, dZ, startPhi, endPhi, lintos, louts, lpini, lpend);
0523   if (status & m_inconsistency) { // Inconsistency => Drop current "sim_hit"
0524     error(inconsistency(header, status, sim_hit.getCellID(), lpos, lmom));
0525     return false;
0526   }
0527   cIDs.push_back(sim_hit.getCellID());
0528   std::vector<int> subHitList;
0529   if (level() >= algorithms::LogLevel::kDebug) {
0530     subHitList.push_back(idx);
0531   }
0532   // Continuations?
0533   bool isContinuation  = status & (m_intoLower | m_intoUpper);
0534   bool hasContinuation = status & (m_outLower | m_outUpper);
0535   bool canReEnter      = status & m_canReEnter;
0536   int rank             = m_stripRank(vID);
0537   if (!canReEnter) {
0538     if (rank == 0 && (status & m_intoLower))
0539       isContinuation = false;
0540     if (rank == 0 && (status & m_outLower))
0541       hasContinuation = false;
0542   }
0543   if (rank == 4 && (status & m_intoUpper))
0544     isContinuation = false;
0545   if (rank == 4 && (status & m_outUpper))
0546     hasContinuation = false;
0547   if (hasContinuation) {
0548     // ***** LOOP OVER HITS
0549     int jdx;
0550     CellID vIDPrv   = vID;
0551     size_t sim_size = sim_hits->size();
0552     for (jdx = idx + 1; jdx < (int)sim_size; jdx++) {
0553       const edm4hep::SimTrackerHit& sim_hjt = sim_hits->at(jdx);
0554       CellID vJD                            = sim_hjt.getCellID() & m_volumeBits;
0555       // Particle may start inward and re-enter, being then outward-going.
0556       // => Orientation has to be evaluated w.r.t. previous vID.
0557       int orientation = m_orientation(vIDPrv, vJD);
0558       bool isUpstream = m_isUpstream(orientation, status);
0559       bool pmoStatus  = samePMO(sim_hit, sim_hjt);
0560       if (!pmoStatus || !isUpstream) {
0561         if ((pmoStatus && !isUpstream) && !sim_hit.isProducedBySecondary()) {
0562           // Bizarre, except if it's a low energy stuff (when it then can be a
0563           // looping particle). If it's not let's flag the case, for debugging.
0564           double P = sqrt(lmom[0] * lmom[0] + lmom[1] * lmom[1] + lmom[2] * lmom[2]);
0565           if (P > 10 * dd4hep::MeV)
0566             debug(inconsistency(header, 0, sim_hit.getCellID(), lpos, lmom));
0567         }
0568         break;
0569       }
0570       // Get 'j' radii
0571       curVol           = volman.lookupDetElement(vJD);
0572       const Tube& tubj = curVol.solid();
0573       rMin             = tubj.rMin();
0574       rMax             = tubj.rMax();
0575       double lpoj[3], lmoj[3];
0576       getLocalPosMom(sim_hjt, toRefVol, lpoj, lmoj);
0577       // Is TRAVERSING through the (quasi-)common wall?
0578       double ljns[2][3], lovts[2][3], lpjni[3], lpfnd[3];
0579       status =
0580           cTraversing(lpoj, lmoj, sim_hjt.getPathLength() * ed2dd, sim_hit.isProducedBySecondary(),
0581                       rMin, rMax, dZ, startPhi, endPhi, ljns, lovts, lpjni, lpfnd);
0582       if (status & m_inconsistency) { // Inconsistency => Drop current "sim_hjt"
0583         error(inconsistency(header, status, sim_hjt.getCellID(), lpoj, lmoj));
0584         break;
0585       }
0586       // ij-Compatibility: status
0587       bool jsDownstream = m_isDownstream(orientation, status);
0588       if (!jsDownstream)
0589         break;
0590       // ij-Compatibility: close exit/entrance-distance
0591       double dist = outInDistance(0, orientation, ljns, louts, lmom, lmoj);
0592       // RELAXED TOLERANCE for low energy stuff
0593       double P          = sqrt(lmom[0] * lmom[0] + lmom[1] * lmom[1] + lmom[2] * lmom[2]);
0594       double tolerance  = m_toleranceFactor(P) * 25 * dd4hep::um;
0595       bool isCompatible = dist > 0 && dist < tolerance;
0596       if (!isCompatible) {
0597         if (!sim_hit.isProducedBySecondary())
0598           debug(oddity(header, status, dist, sim_hit.getCellID(), lpos, lmom,
0599                        /* */ sim_hjt.getCellID(), lpoj, lmoj));
0600         break;
0601       }
0602       // ***** UPDATE
0603       vIDPrv = vJD;
0604       eDep += sim_hjt.getEDep();
0605       for (int i = 0; i < 3; i++) { // Update end point position/momentum.
0606         lpend[i] = lpfnd[i];
0607         lmend[i] = lmoj[i];
0608       }
0609       // ***** BOOK-KEEPING
0610       cIDs.push_back(sim_hjt.getCellID());
0611       if (level() >= algorithms::LogLevel::kDebug) {
0612         subHitList.push_back(jdx);
0613       }
0614       // ***** CONTINUATION?
0615       hasContinuation = status & 0xa;
0616       canReEnter      = status & 0x100;
0617       if (!canReEnter && m_stripRank(vJD) == 4)
0618         hasContinuation = false;
0619       if (!hasContinuation) {
0620         jdx++;
0621         break;
0622       } else { // Update outgoing position/momentum for next iteration.
0623         for (int i = 0; i < 3; i++) {
0624           louts[0][i] = lovts[0][i];
0625           louts[1][i] = lovts[1][i];
0626         }
0627       }
0628     }
0629     idx = jdx - 1;
0630   }
0631   // ***** EXTENSION?...
0632   if (sim_hit.isProducedBySecondary() && cIDs.size() < 2)
0633     if (denyExtension(sim_hit, tCur.rMax() - tCur.rMin())) {
0634       isContinuation = hasContinuation = false;
0635     }
0636   for (int io = 0; io < 2; io++) { // ...into/out-of
0637     if ((io == 0 && !isContinuation) || (io == 1 && !hasContinuation))
0638       continue;
0639     int direction = io ? +1 : -1;
0640     extendHit(refID, cIDs, direction, lpini, lmom, lpend, lmend);
0641   }
0642   // ***** FLAG CASES W/ UNEXPECTED OUTCOME
0643   flagUnexpected(header, 0, (tRef.rMin() + tRef.rMax()) / 2, sim_hit, lpini, lpend, lpos, lmom);
0644   // ***** UPDATE (local position <lpos>, DoF)
0645   double DoF2 = 0, dir = 0;
0646   for (int i = 0; i < 3; i++) {
0647     double neu = (lpini[i] + lpend[i]) / 2, alt = lpos[i];
0648     lpos[i]  = neu;
0649     double d = neu - alt;
0650     dir += d * lmom[i];
0651     DoF2 += d * d;
0652   }
0653   // Update time by ToF from original subHit to extended/COALESCED.
0654   time += ((dir > 0) ? 1 : ((dir < 0) ? -1 : 0)) * sqrt(DoF2) / dd4hep::c_light;
0655   if (level() >= algorithms::LogLevel::kDebug) {
0656     debug("--------------------");
0657     printSubHitList(input, subHitList);
0658     debug("  =");
0659     // Print position, eDep and time of coalesced/extended hit
0660     Position locPos(lpos[0], lpos[1], lpos[2]); // Simplification: strip surface = REFERENCE surface
0661     Position globPos = refVol.nominal().localToWorld(locPos);
0662     debug("  position  = ({:7.2f},{:7.2f},{:7.2f}) [mm]", globPos.X() / mm, globPos.Y() / mm,
0663           globPos.Z() / mm);
0664     debug("  edep = {:.0f} [eV]", eDep / eV);
0665     debug("  time = {:.2f} [ns]", time);
0666   }
0667   return true;
0668 }
0669 bool MPGDTrackerDigi::bCoalesceExtend(const Input& input, int& idx,
0670                                       std::vector<std::uint64_t>& cIDs, double* lpos, double& eDep,
0671                                       double& time) const {
0672   const auto [headers, sim_hits]        = input;
0673   const edm4hep::EventHeader& header    = headers->at(0);
0674   const edm4hep::SimTrackerHit& sim_hit = sim_hits->at(idx);
0675   CellID vID                            = sim_hit.getCellID() & m_volumeBits;
0676   CellID refID                          = vID & m_moduleBits; // => The REFERENCE SUBVOLUME
0677   const VolumeManager& volman           = m_detector->volumeManager();
0678   DetElement refVol                     = volman.lookupDetElement(refID);
0679   // TGeoHMatrix: In order to avoid a "dangling-reference" warning, let's take
0680   // a copy of the matrix instead of a reference to it.
0681   const TGeoHMatrix toRefVol = refVol.nominal().worldTransformation();
0682   double lmom[3];
0683   getLocalPosMom(sim_hit, toRefVol, lpos, lmom);
0684   const double edmm = edm4eic::unit::mm, ed2dd = dd4hep::mm / edmm;
0685   using dd4hep::mm;
0686   using edm4eic::unit::eV, edm4eic::unit::GeV;
0687   // Hit in progress
0688   eDep = sim_hit.getEDep();
0689   time = sim_hit.getTime();
0690   // Get VOLUME parameters
0691   const Box& bRef = refVol.solid(); // REFERENCE SUBVOLUME
0692   double dX = bRef.x(), dY = bRef.y();
0693   // Get current SUBVOLUME
0694   DetElement curVol = volman.lookupDetElement(vID);
0695   const Box& bCur   = curVol.solid();
0696   double dZ         = bCur.z();
0697   double ref2Cur    = getRef2Cur(refVol, curVol);
0698   // Is TRAVERSING?
0699   double lintos[2][3], louts[2][3], lpini[3], lpend[3], lmend[3];
0700   std::copy(std::begin(lmom), std::end(lmom), std::begin(lmend));
0701   unsigned int status =
0702       bTraversing(lpos, lmom, ref2Cur, sim_hit.getPathLength() * ed2dd,
0703                   sim_hit.isProducedBySecondary(), dZ, dX, dY, lintos, louts, lpini, lpend);
0704   if (status & m_inconsistency) { // Inconsistency => Drop current "sim_hit"
0705     error(inconsistency(header, status, sim_hit.getCellID(), lpos, lmom));
0706     return false;
0707   }
0708   cIDs.push_back(sim_hit.getCellID());
0709   std::vector<int> subHitList;
0710   if (level() >= algorithms::LogLevel::kDebug) {
0711     subHitList.push_back(idx);
0712   }
0713   // Continuations?
0714   int rank             = m_stripRank(vID);
0715   bool isContinuation  = status & (m_intoLower | m_intoUpper);
0716   bool hasContinuation = status & (m_outLower | m_outUpper);
0717   if ((rank == 0 && (status & m_intoLower)) || (rank == 4 && (status & m_intoUpper)))
0718     isContinuation = false;
0719   if ((rank == 0 && (status & m_outLower)) || (rank == 4 && (status & m_outUpper)))
0720     hasContinuation = false;
0721   if (hasContinuation) {
0722     // ***** LOOP OVER SUBHITS
0723     int jdx;
0724     CellID vIDPrv   = vID;
0725     size_t sim_size = sim_hits->size();
0726     for (jdx = idx + 1; jdx < (int)sim_size; jdx++) {
0727       const edm4hep::SimTrackerHit& sim_hjt = sim_hits->at(jdx);
0728       CellID vJD                            = sim_hjt.getCellID() & m_volumeBits;
0729       int orientation                       = m_orientation(vIDPrv, vJD);
0730       bool isUpstream                       = m_isUpstream(orientation, status);
0731       bool pmoStatus                        = samePMO(sim_hit, sim_hjt);
0732       if (!pmoStatus || !isUpstream) {
0733         if ((pmoStatus && !isUpstream) && !sim_hit.isProducedBySecondary()) {
0734           // Bizarre: let's flag the case for debugging, if not low energy.
0735           double P = sqrt(lmom[0] * lmom[0] + lmom[1] * lmom[1] + lmom[2] * lmom[2]);
0736           if (P > 10 * dd4hep::MeV)
0737             debug(inconsistency(header, 0, sim_hit.getCellID(), lpos, lmom));
0738         }
0739         break;
0740       }
0741       // Get 'j' Z
0742       curVol          = volman.lookupDetElement(vJD); // 'j' SUBVOLUME
0743       const Box& boxj = curVol.solid();
0744       dZ              = boxj.z();
0745       double ref2j    = getRef2Cur(refVol, curVol);
0746       // Is TRAVERSING through the (quasi)-common border?
0747       double lpoj[3], lmoj[3];
0748       getLocalPosMom(sim_hjt, toRefVol, lpoj, lmoj);
0749       double ljns[2][3], lovts[2][3], lpjni[3], lpfnd[3];
0750       status = bTraversing(lpoj, lmoj, ref2j, sim_hjt.getPathLength() * ed2dd,
0751                            sim_hit.isProducedBySecondary(), dZ, dX, dY, ljns, lovts, lpjni, lpfnd);
0752       if (status & m_inconsistency) { // Inconsistency => Drop current "sim_hjt"
0753         error(inconsistency(header, status, sim_hjt.getCellID(), lpoj, lmoj));
0754         break;
0755       }
0756       // ij-Compatibility: status
0757       bool jsDownstream = m_isDownstream(orientation, status);
0758       if (!jsDownstream)
0759         break;
0760       // ij-Compatibility: close exit/entrance-distance
0761       double dist = outInDistance(1, orientation, ljns, louts, lmom, lmoj);
0762       // RELAXED TOLERANCE for low energy stuff
0763       double P          = sqrt(lmom[0] * lmom[0] + lmom[1] * lmom[1] + lmom[2] * lmom[2]);
0764       double tolerance  = m_toleranceFactor(P) * 25 * dd4hep::um;
0765       bool isCompatible = dist > 0 && dist < tolerance;
0766       if (!isCompatible) {
0767         if (!sim_hit.isProducedBySecondary())
0768           debug(oddity(header, status, dist, sim_hit.getCellID(), lpos, lmom,
0769                        /* */ sim_hjt.getCellID(), lpoj, lmoj));
0770         break;
0771       }
0772       // ***** UPDATE
0773       vIDPrv = vJD;
0774       eDep += sim_hjt.getEDep();
0775       for (int i = 0; i < 3; i++) { // Update end point position/momentum.
0776         lpend[i] = lpfnd[i];
0777         lmend[i] = lmoj[i];
0778       }
0779       // ***** BOOK-KEEPING
0780       cIDs.push_back(sim_hjt.getCellID());
0781       if (level() >= algorithms::LogLevel::kDebug) {
0782         subHitList.push_back(jdx);
0783       }
0784       // ***** CONTINUATION?
0785       hasContinuation = status & 0xa;
0786       if (!hasContinuation) {
0787         jdx++;
0788         break;
0789       } else { // Update outgoing position/momentum for next iteration.
0790         for (int i = 0; i < 3; i++) {
0791           louts[0][i] = lovts[0][i];
0792           louts[1][i] = lovts[1][i];
0793         }
0794       }
0795     }
0796     idx = jdx - 1;
0797   }
0798   // ***** EXTENSION?...
0799   if (sim_hit.isProducedBySecondary() && cIDs.size() < 2)
0800     if (denyExtension(sim_hit, bCur.z())) {
0801       isContinuation = hasContinuation = false;
0802     }
0803   for (int io = 0; io < 2; io++) { // ...into/out-of
0804     if ((io == 0 && !isContinuation) || (io == 1 && !hasContinuation))
0805       continue;
0806     int direction = io ? +1 : -1;
0807     extendHit(refID, cIDs, direction, lpini, lmom, lpend, lmend);
0808   }
0809   // ***** FLAG CASES W/ UNEXPECTED OUTCOME
0810   flagUnexpected(header, 1, 0, sim_hit, lpini, lpend, lpos, lmom);
0811   // ***** UPDATE (local position <lpos>, DoF)
0812   double DoF2 = 0, dir = 0;
0813   for (int i = 0; i < 3; i++) {
0814     double neu = (lpini[i] + lpend[i]) / 2, alt = lpos[i];
0815     lpos[i]  = neu;
0816     double d = neu - alt;
0817     dir += d * lmom[i];
0818     DoF2 += d * d;
0819   }
0820   // Update time by ToF from original subHit to extended/COALESCED.
0821   time += ((dir > 0) ? 1 : ((dir < 0) ? -1 : 0)) * sqrt(DoF2) / dd4hep::c_light;
0822   if (level() >= algorithms::LogLevel::kDebug) {
0823     debug("--------------------");
0824     printSubHitList(input, subHitList);
0825     debug("  =");
0826     // Print position, eDep and time of coalesced/extended hit
0827     Position locPos(lpos[0], lpos[1], lpos[2]); // Simplification: strip surface = REFERENCE surface
0828     Position globPos = refVol.nominal().localToWorld(locPos);
0829     debug("  position  = ({:7.2f},{:7.2f},{:7.2f}) [mm]", globPos.X() / mm, globPos.Y() / mm,
0830           globPos.Z() / mm);
0831     debug("  edep = {:.0f} [eV]", eDep / eV);
0832     debug("  time = {:.2f} [ns]", time);
0833   }
0834   return true;
0835 }
0836 void MPGDTrackerDigi::printSubHitList(const Input& input, std::vector<int>& subHitList) const {
0837   const auto [headers, sim_hits] = input;
0838   const double edmm              = edm4eic::unit::mm;
0839   using edm4eic::unit::eV, edm4eic::unit::GeV;
0840   int ldx = 0;
0841   for (int kdx : subHitList) {
0842     const edm4hep::SimTrackerHit& sim_hp = sim_hits->at(kdx);
0843     CellID cIDk                          = sim_hp.getCellID();
0844     CellID hIDk = cIDk >> 32, vIDk = cIDk & m_volumeBits;
0845     if (ldx == 0) {
0846       debug("Hit cellID{:d} = 0x{:08x}, 0x{:08x}", ldx++, hIDk, vIDk);
0847     } else {
0848       debug("  + cellID{:d} = 0x{:08x}, 0x{:08x}", ldx++, hIDk, vIDk);
0849     }
0850     debug("  position  = ({:7.2f},{:7.2f},{:7.2f}) [mm]", sim_hp.getPosition().x / edmm,
0851           sim_hp.getPosition().y / edmm, sim_hp.getPosition().z / edmm);
0852     debug("  xy_radius = {:.2f}",
0853           std::hypot(sim_hp.getPosition().x, sim_hp.getPosition().y) / edmm);
0854     debug("  momentum  = ({:.2f}, {:.2f}, {:.2f}) [GeV]", sim_hp.getMomentum().x / GeV,
0855           sim_hp.getMomentum().y / GeV, sim_hp.getMomentum().z / GeV);
0856     debug("  edep = {:.0f} [eV]", sim_hp.getEDep() / eV);
0857     debug("  time = {:.2f} [ns]", sim_hp.getTime());
0858   }
0859 }
0860 
0861 void getLocalPosMom(const edm4hep::SimTrackerHit& sim_hit, const TGeoHMatrix& toModule,
0862                     double* lpos, double* lmom) {
0863   const edm4hep::Vector3d& pos = sim_hit.getPosition();
0864   // Length: Inputs are in EDM4eic units. Let's move to DD4hep units.
0865   const double edmm = edm4eic::unit::mm, ed2dd = dd4hep::mm / edmm;
0866   const double gpos[3]         = {pos.x * ed2dd, pos.y * ed2dd, pos.z * ed2dd};
0867   const edm4hep::Vector3f& mom = sim_hit.getMomentum();
0868   const double gmom[3]         = {mom.x, mom.y, mom.z};
0869   toModule.MasterToLocal(gpos, lpos);
0870   toModule.MasterToLocalVect(gmom, lmom);
0871 }
0872 
0873 // ******************** TRAVERSING?
0874 // Particle can be born/dead (then its position is not (entrance+exit)/2).
0875 // Or it can exit through the edge.
0876 // - Returned Status code, see header.
0877 //     Also, for internal use:
0878 //     0x10: Enters through edge
0879 //     0x20: Exits  through edge
0880 // - <lintos>/<louts>: Positions @ lower/upper wall upon Enter-/Exit-ing (when endorsed by <status>)
0881 // - <lpini>/<lpend>: Positions of extrema
0882 // - TOLERANCE? For MIPs, a tolerance of 1 µM works fine. But for lower energy,
0883 //  looks like we need something somewhat larger. The ideal would be to base
0884 //  the value on Molière width. Here, I use a somewhat arbitrary built-in.
0885 //  - If particle found to reach wall, w/in tolerance, assign end points to
0886 //   walls (instead of <lpos>+/-path/2). It will make so that the eventual
0887 //   extrapolated position falls exactly at mid-plane, even in the case of a
0888 //   low energy particle, where path may be affected by multiscattering. This,
0889 //   provided that particle is not a secondary.
0890 unsigned int MPGDTrackerDigi::cTraversing(const double* lpos, const double* lmom, double path,
0891                                           bool isSecondary,         // Input subHit
0892                                           double rMin, double rMax, // Current instance of SUBVOLUME
0893                                           double dZ, double startPhi,
0894                                           double endPhi, // Module parameters
0895                                           double lintos[][3], double louts[][3], double* lpini,
0896                                           double* lpend) const {
0897   unsigned int status = 0;
0898   double Mx = lpos[0], My = lpos[1], Mz = lpos[2], M2 = Mx * Mx + My * My;
0899   double Px = lmom[0], Py = lmom[1], Pz = lmom[2];
0900   // Intersection w/ the edge in phi
0901   double tIn = 0, tOut = 0;
0902   for (double phi : {startPhi, endPhi}) {
0903     // M+t*P = 0 + t'*U. t = (My*Ux-Mx*Uy)/(Px*Uy-Py*Ux);
0904     double Ux = cos(phi), Uy = sin(phi);
0905     double D = Px * Uy - Py * Ux;
0906     if (D) { // If P not // to U
0907       double t  = (My * Ux - Mx * Uy) / D;
0908       double Ex = Mx + t * Px, Ey = My + t * Py, Ez = Mz + t * Pz;
0909       double rE = sqrt(Ex * Ex + Ey * Ey), phiE = atan2(Ey, Ex);
0910       // The above does not distinguish between phi and phi+pi.
0911       // => Have to explicitly discard the latter.
0912       if (rMin < rE && rE < rMax && fabs(Ez) < dZ && fabs(phiE - phi) < 1) {
0913         if (t < 0) {
0914           status |= 0x10;
0915           tIn = t;
0916         } else {
0917           status |= 0x20;
0918           tOut = t;
0919         }
0920       }
0921     }
0922   }
0923   // Intersection w/ the edge in Z
0924   double zLow = -dZ, zUp = +dZ;
0925   for (double Z : {zLow, zUp}) {
0926     // Mz+t*Pz = Z
0927     if (Pz) {
0928       double t  = (Z - Mz) / Pz;
0929       double Ex = Mx + t * Px, Ey = My + t * Py, rE = sqrt(Ex * Ex + Ey * Ey);
0930       double phi = atan2(Ey, Ex);
0931       if (rMin < rE && rE < rMax && startPhi < phi && phi < endPhi) {
0932         if (t < 0) {
0933           if (!(status & 0x10) || ((status & 0x10) && t > tIn)) {
0934             status |= 0x10;
0935             tIn = t;
0936           }
0937         } else if (t > 0) {
0938           if (!(status & 0x20) || ((status & 0x20) && t < tOut)) {
0939             status |= 0x20;
0940             tOut = t;
0941           }
0942         }
0943       }
0944     }
0945   }
0946   // Intersection w/ tube walls
0947   double ts[3 /* rMin/rMax/edge */][2 /* In/Out */] = {
0948       {0, 0}, {0, 0}, {tIn, tOut}}; // Up to two intersections
0949   double a = Px * Px + Py * Py, b = Px * Mx + Py * My;
0950   for (int lu = 0; lu < 2; lu++) { // rMin/rMax
0951     double R;
0952     unsigned int statGene;
0953     if (lu == 1) {
0954       R        = rMax;
0955       statGene = 0x4;
0956     } else {
0957       R        = rMin;
0958       statGene = 0x1;
0959     }
0960     double c = M2 - R * R;
0961     if (!a) { // P is // to Z. Yet no intersect w/ Z edge.
0962       if ((status & 0x30) != 0x30)
0963         status |= 0x1000;
0964       continue;      // Inconsistency
0965     } else if (!c) { // Hit is on wall: inconsistency.
0966       status |= 0x2000;
0967       continue;
0968     } else {
0969       double det = b * b - a * c;
0970       if (det < 0) {
0971         if (lu == 1) { // No intersection w/ outer wall: inconsistency.
0972           status |= 0x4000;
0973           continue;
0974         }
0975       } else {
0976         double sqdet = sqrt(det);
0977         for (int is = 0; is < 2; is++) {
0978           int s     = 1 - 2 * is;
0979           double t  = (-b + s * sqdet) / a;
0980           double Ix = Mx + t * Px, Iy = My + t * Py, Iz = Mz + t * Pz, phi = atan2(Iy, Ix);
0981           if (fabs(Iz) > dZ || phi < startPhi || endPhi < phi)
0982             continue;
0983           if (t < 0) {
0984             // Two rMin intersects in same back/forward direction may happen
0985             // (one and and only one of them may then be hidden by edge).
0986             // => Have to allow wall intersect to coexist w/ edge intersect.
0987             //   This only for rMin, but for simplicity's sake...
0988             if (status & statGene) { // Two <0 ts: can only happen when rMin
0989               double tPrv = ts[lu][0];
0990               if (t > tPrv) {
0991                 ts[lu][0] = t;
0992                 ts[lu][1] = tPrv; // Current is actually IN, previous is OUT despite being <0
0993               }
0994               status |= statGene << 1;
0995             } else {
0996               ts[lu][0] = t;
0997               status |= statGene;
0998             }
0999           } else {                        // (if t > 0)
1000             if (status & statGene << 1) { // Two >0 ts: can only happen when rMin
1001               double tPrv = ts[lu][1];
1002               if (t < tPrv) {
1003                 ts[lu][1] = t;
1004                 ts[lu][0] = tPrv; // Current is actually OUT, previous is IN despite being >0
1005               }
1006               status |= statGene;
1007             } else {
1008               ts[lu][1] = t;
1009               status |= statGene << 1;
1010             }
1011           }
1012         }
1013       }
1014     }
1015   }
1016   // Combine w/ edge in/out, based on "t".
1017   // - A priori, wall crossing (conditioned by w/in edges) and edge crossing (
1018   //  conditioned by rMin<rE<rMax) are mutually exclusive...
1019   // - ...This, except when particle re-enters through the lower wall...
1020   // =>
1021   //  - No reEntrance: Let's double-check that the above holds, canceling wall
1022   //   crossing and raising an inconsistency status bit when not.
1023   //  - Else:
1024   //    - If doesReEnter, reEntrance is a mere transient trip outside the
1025   //     SUBVOLUME. => Cancel it.
1026   //    - Else disregard edge, possibly raising an inconsistency status bit.
1027   // Hit lies outside?
1028   // - ...Or when hit position lies outside volume (it can happen, thanks to
1029   //  limited precision).
1030   //  => If this is the case, let's swap their exit vs. entrance status. This
1031   //  will prevent inconsistencies from showing up below.
1032   // Can reEnter: does reEnter?
1033   bool canReEnter = (status & 0x3) == 0x3;
1034   double norm     = sqrt(a + Pz * Pz);
1035   if (canReEnter) {
1036     bool doesReEnter = true;
1037     for (int i12 = 0; i12 < 2; i12++) {
1038       double t               = ts[0][i12];
1039       int s                  = t > 0 ? +1 : -1;
1040       const double tolerance = 20 * dd4hep::um;
1041       if (path / 2 - s * t * norm < tolerance)
1042         doesReEnter = false;
1043     }
1044     if (doesReEnter) {
1045       status &= ~0x3;
1046       canReEnter = false;
1047     }
1048   }
1049   double rHit = sqrt(M2);
1050   if (rHit < rMin && !canReEnter) {
1051     unsigned int statvs = status;
1052     if (statvs & 0x1) {
1053       status &= ~0x1;
1054       status |= 0x2;
1055       ts[0][1] = -ts[0][0];
1056     }
1057     if (statvs & 0x2) {
1058       status &= ~0x2;
1059       status |= 0x1;
1060       ts[0][0] = -ts[0][1];
1061     }
1062   } else if (rHit > rMax) {
1063     unsigned int statvs = status;
1064     if (statvs & 0x4) {
1065       status &= ~0x4;
1066       status |= 0x8;
1067       ts[1][1] = -ts[1][0];
1068     }
1069     if (statvs & 0x8) {
1070       status &= ~0x8;
1071       status |= 0x4;
1072       ts[1][0] = -ts[1][1];
1073     }
1074   }
1075   for (int lu = 0; lu < 2; lu++) { // rMin/rMax
1076     unsigned int statGene = lu ? 0x4 : 0x1;
1077     if (status & statGene) {
1078       double t = ts[lu][0];
1079       if (t < 0) {
1080         if (status & 0x10) {
1081           if (lu == 1 || !canReEnter) { // No reEntrance:
1082             status |= 0x10000;          //   Inconsistency
1083             status &= ~statGene;        //   Cancel wall crossing
1084           } else {                      // ReEntrance: disregard edge crossing
1085             if (t < tIn)
1086               status |= 0x10000; // Inconsistency
1087           }
1088         }
1089       } else { // if (t > 0)
1090         if (status & 0x20) {
1091           if (lu == 1 || !canReEnter) {
1092             status |= 0x20000;
1093             status &= ~statGene;
1094           } else {
1095             if (t > tOut)
1096               status |= 0x20000;
1097           }
1098         }
1099       }
1100     }
1101     if (status & statGene << 1) {
1102       double t = ts[lu][1];
1103       if (t < 0) {
1104         if (status & 0x10) {
1105           if (lu == 1 || !canReEnter) {
1106             status |= 0x40000;
1107             status &= ~(statGene << 1);
1108           } else {
1109             if (t < tIn)
1110               status |= 0x40000;
1111           }
1112         }
1113       } else { // if (t > 0)
1114         if (status & 0x20) {
1115           if (lu == 1 || !canReEnter) {
1116             status |= 0x80000;
1117             status &= ~(statGene << 1);
1118           } else {
1119             if (t > tOut)
1120               status |= 0x80000;
1121           }
1122         }
1123       }
1124     }
1125   }
1126   // Is particle born/dead prior to entering/exiting?
1127   // - sim_hit must have been assigned the mean position: (entrance+exit)/2
1128   // - Let's then check entrance/exit against sim_hit's position +/- path/2.
1129   //   When the latter is too short, it means that the particle firing the
1130   //  hit gets born or dies in the SUBVOLUME ("dying" taken here in the broad
1131   //  sense of undergoing a discrete physics process).
1132   // => We remove the corresponding bit in the <status> pattern.
1133   // - Note that we not only require that the path be long enough, but also
1134   //  that it matches exactly distances to entrance/exit.
1135   if (canReEnter)
1136     status |= 0x100; // Remember that particle can re-enter.
1137   double at           = path / 2 / norm;
1138   unsigned int statws = 0;
1139   for (int is = 0; is < 2; is++) {
1140     int s     = 1 - 2 * is;
1141     double Ix = s * at * Px, Iy = s * at * Py, Iz = s * at * Pz;
1142     for (int lu = 0; lu < 2; lu++) { // Lower/upper wall
1143       unsigned int statvs = lu ? 0x4 : 0x1;
1144       for (int io = 0; io < 2; io++) {
1145         statvs <<= io;
1146         if (status & statvs) {
1147           double t = ts[lu][io];
1148           if (t * s < 0)
1149             continue;
1150           double dIx = t * Px - Ix, dIy = t * Py - Iy, dIz = t * Pz - Iz;
1151           double dist = sqrt(dIx * dIx + dIy * dIy + dIz * dIz);
1152           // RELAXED TOLERANCE for low energy stuff
1153           double tolerance = m_toleranceFactor(norm) * 20 * dd4hep::um;
1154           if (dist < tolerance)
1155             statws |= statvs;
1156         }
1157       }
1158     }
1159   }
1160   if (!(statws & 0x5)) /* No entrance */
1161     status &= ~0x5;
1162   if (!(statws & 0xa)) /* No exit */
1163     status &= ~0xa;
1164   // ***** End points
1165   // Assign end points to walls, if not a secondary and provided it's not a
1166   // reEntrance case, which case is more difficult to handle and we leave aside.
1167   if (((status & 0x5) == 0x1 || (status & 0x5) == 0x4) && !isSecondary) {
1168     double tIn = (status & 0x1) ? ts[0][0] : ts[1][0];
1169     lpini[0]   = Mx + tIn * Px;
1170     lpini[1]   = My + tIn * Py;
1171     lpini[2]   = Mz + tIn * Pz;
1172   } else {
1173     lpini[0] = Mx - at * Px;
1174     lpini[1] = My - at * Py;
1175     lpini[2] = Mz - at * Pz;
1176   }
1177   if (((status & 0xa) == 0x2 || (status & 0xa) == 0x8) && !isSecondary) {
1178     double tOut = (status & 0x2) ? ts[0][1] : ts[1][1];
1179     lpend[0]    = Mx + tOut * Px;
1180     lpend[1]    = My + tOut * Py;
1181     lpend[2]    = Mz + tOut * Pz;
1182   } else {
1183     lpend[0] = Mx + at * Px;
1184     lpend[1] = My + at * Py;
1185     lpend[2] = Mz + at * Pz;
1186   }
1187   // End points when on the walls
1188   for (int lu = 0; lu < 2; lu++) {
1189     unsigned int statvs = lu ? 0x4 : 0x1;
1190     double tIn = ts[lu][0], tOut = ts[lu][1];
1191     if (status & statvs) {
1192       lintos[lu][0] = Mx + tIn * Px;
1193       lintos[lu][1] = My + tIn * Py;
1194       lintos[lu][2] = Mz + tIn * Pz;
1195     }
1196     statvs <<= 1;
1197     if (status & statvs) {
1198       louts[lu][0] = Mx + tOut * Px;
1199       louts[lu][1] = My + tOut * Py;
1200       louts[lu][2] = Mz + tOut * Pz;
1201     }
1202   }
1203   return status;
1204 }
1205 unsigned int MPGDTrackerDigi::bTraversing(const double* lpos, const double* lmom, double ref2Cur,
1206                                           double path,
1207                                           bool isSecondary,     // Input subHit
1208                                           double dZ,            // Current instance of SUBVOLUME
1209                                           double dX, double dY, // Module parameters
1210                                           double lintos[][3], double louts[][3], double* lpini,
1211                                           double* lpend) const {
1212   unsigned int status = 0;
1213   double Mx = lpos[0], My = lpos[1], Mxy[2] = {Mx, My};
1214   double Px = lmom[0], Py = lmom[1], Pxy[2] = {Px, Py};
1215   double Mz = lpos[2] + ref2Cur, Pz = lmom[2];
1216   // Intersection w/ the edge in X,Y
1217   double tIn = 0, tOut = 0;
1218   double xyLow[2] = {-dX, -dY}, xyUp[2] = {+dX, +dY};
1219   for (int xy = 0; xy < 2; xy++) {
1220     int yx       = 1 - xy;
1221     double a_Low = xyLow[xy], a_Up = xyUp[xy], Ma = Mxy[xy], Pa = Pxy[xy];
1222     double b_Low = xyLow[yx], b_Up = xyUp[yx], Mb = Mxy[yx], Pb = Pxy[yx];
1223     for (double A : {a_Low, a_Up}) {
1224       // Ma+t*Pa = A
1225       if (Pa) {
1226         double t  = (A - Ma) / Pa;
1227         double Eb = Mb + t * Pb, Ez = Mz + t * Pz;
1228         if (b_Low < Eb && Eb < b_Up && fabs(Ez) < dZ) {
1229           if (t < 0) {
1230             if (!(status & 0x10) || ((status & 0x10) && t > tIn)) {
1231               status |= 0x10;
1232               tIn = t;
1233             }
1234           } else if (t > 0) {
1235             if (!(status & 0x20) || ((status & 0x20) && t < tOut)) {
1236               status |= 0x20;
1237               tOut = t;
1238             }
1239           }
1240         }
1241       }
1242     }
1243   }
1244   // Intersection w/ box walls
1245   for (int lu = 0; lu < 2; lu++) {
1246     int s                 = 2 * lu - 1;
1247     double Z              = s * dZ;
1248     unsigned int statGene = lu ? 0x4 : 0x1;
1249     // Mz+t*Pz = Z
1250     if (Pz) {
1251       double t = (Z - Mz) / Pz;
1252       if (t < 0) {
1253         if (!(status & 0x10) || ((status & 0x10) && t > tIn)) {
1254           status |= statGene;
1255           tIn = t;
1256         }
1257       } else if (t > 0) {
1258         if (!(status & 0x20) || ((status & 0x20) && t < tOut)) {
1259           status |= statGene << 1;
1260           tOut = t;
1261         }
1262       }
1263     }
1264   }
1265   // Is particle born/dead prior to entering/exiting?
1266   // - sim_hit must have been assigned the mean position: (entrance+exit)/2
1267   // - Let's then check entrance/exit against sim_hit's position +/- path/2.
1268   //   When the latter is too short, it means that the particle firing the
1269   //  hit gets born or dies in the SUBVOLUME ("dying" taken here in the broad
1270   //  sense of not undergoing any discrete physics process).
1271   // => We remove the corresponding bit in the <status> pattern.
1272   // - Note that we not only require that the path be long enough, but also
1273   //  that it matches exactly distances to entrance/exit.
1274   double norm = sqrt(Px * Px + Py * Py + Pz * Pz), at = path / 2 / norm;
1275   unsigned int statws = 0;
1276   for (int is = 0; is < 2; is++) {
1277     int s     = 1 - 2 * is;
1278     double Ix = s * at * Px, Iy = s * at * Py, Iz = s * at * Pz;
1279     for (int lu = 0; lu < 2; lu++) { // Lower/upper wall
1280       unsigned int statvs = lu ? 0x4 : 0x1;
1281       for (int io = 0; io < 2; io++) {
1282         statvs <<= io;
1283         if (status & statvs) {
1284           double t = io ? tOut : tIn;
1285           if (t * s < 0)
1286             continue;
1287           double dIx = t * Px - Ix, dIy = t * Py - Iy, dIz = t * Pz - Iz;
1288           double dist = sqrt(dIx * dIx + dIy * dIy + dIz * dIz);
1289           // RELAXED TOLERANCE for low energy stuff
1290           double tolerance = m_toleranceFactor(norm) * 20 * dd4hep::um;
1291           if (dist < tolerance)
1292             statws |= statvs;
1293         }
1294       }
1295     }
1296   }
1297   if (!(statws & 0x5)) /* No entrance */
1298     status &= ~0x5;
1299   if (!(statws & 0xa)) /* No exit */
1300     status &= ~0xa;
1301   // ***** OUTPUT POSITIONS
1302   Mz -= ref2Cur; // Go back to REFERENCE SUBVOLUME
1303   // End points:
1304   // Assign end points to walls, if not a secondary.
1305   if ((status & 0x5) && !isSecondary) {
1306     lpini[0] = Mx + tIn * Px;
1307     lpini[1] = My + tIn * Py;
1308     lpini[2] = Mz + tIn * Pz;
1309   } else {
1310     lpini[0] = Mx - at * Px;
1311     lpini[1] = My - at * Py;
1312     lpini[2] = Mz - at * Pz;
1313   }
1314   if ((status & 0xa) && !isSecondary) {
1315     lpend[0] = Mx + tOut * Px;
1316     lpend[1] = My + tOut * Py;
1317     lpend[2] = Mz + tOut * Pz;
1318   } else {
1319     lpend[0] = Mx + at * Px;
1320     lpend[1] = My + at * Py;
1321     lpend[2] = Mz + at * Pz;
1322   }
1323   // End points when on the walls:
1324   for (int lu = 0; lu < 2; lu++) {
1325     unsigned int statvs = lu ? 0x4 : 0x1;
1326     if (status & statvs) {
1327       lintos[lu][0] = Mx + tIn * Px;
1328       lintos[lu][1] = My + tIn * Py;
1329       lintos[lu][2] = Mz + tIn * Pz;
1330     }
1331     statvs <<= 1;
1332     if (status & statvs) {
1333       louts[lu][0] = Mx + tOut * Px;
1334       louts[lu][1] = My + tOut * Py;
1335       louts[lu][2] = Mz + tOut * Pz;
1336     }
1337   }
1338   return status;
1339 }
1340 
1341 // ***** EXTRAPOLATE
1342 bool cExtrapolate(const double* lpos, const double* lmom, // Input subHit
1343                   double rT,                              // Target radius
1344                   double* lext)                           // Extrapolated position @ <rT>
1345 {
1346   bool ok   = false;
1347   double Mx = lpos[0], My = lpos[1], Mz = lpos[2], M2 = Mx * Mx + My * My;
1348   double Px = lmom[0], Py = lmom[1], Pz = lmom[2];
1349   double a = Px * Px + Py * Py, b = Px * Mx + Py * My, c = M2 - rT * rT;
1350   double tF = 0;
1351   if (!c)
1352     ok = true;
1353   else if (a) { // P is not // to Z
1354     double det = b * b - a * c;
1355     if (det >= 0) {
1356       double sqdet = sqrt(det);
1357       for (int is = 0; is < 2; is++) {
1358         int s    = 1 - 2 * is;
1359         double t = (-b + s * sqdet) / a, norm = sqrt(a + Pz * Pz);
1360         // "t" may happen to be slightly <0, because of limited precision
1361         if (t * norm < -dd4hep::nm)
1362           continue;
1363         if (!ok ||
1364             // Two intersects: let's retain the earliest one.
1365             (ok && fabs(t) < fabs(tF))) {
1366           tF = t;
1367           ok = true;
1368         }
1369       }
1370     }
1371   }
1372   if (ok) {
1373     lext[0] = Mx + tF * Px;
1374     lext[1] = My + tF * Py;
1375     lext[2] = Mz + tF * Pz;
1376   }
1377   return ok;
1378 }
1379 bool bExtrapolate(const double* lpos, const double* lmom, // Input subHit
1380                   double zT,                              // Target Z
1381                   double* lext)                           // Extrapolated position @ <zT>
1382 {
1383   bool ok   = false;
1384   double Mx = lpos[0], My = lpos[1], Mz = lpos[2];
1385   double Px = lmom[0], Py = lmom[1], Pz = lmom[2], norm = sqrt(Px * Px + Py * Py + Pz * Pz);
1386   double tF = 0;
1387   if (Pz) {
1388     tF = (zT - Mz) / Pz;
1389     // "t" may happen to be slightly <0, because of limited precision
1390     ok = tF * norm > -dd4hep::nm;
1391   }
1392   if (ok) {
1393     lext[0] = Mx + tF * Px;
1394     lext[1] = My + tF * Py;
1395     lext[2] = Mz + tF * Pz;
1396   }
1397   return ok;
1398 }
1399 
1400 // ***** EXTENSION
1401 // At variance to EXTRAPOLATION, we take edges (phi,Z/X,Y) into account.
1402 // - Returns 0x1 if
1403 //   - there is an extrapolation between position <lpos> and target,
1404 //   - within edge limits,
1405 //   - along momentum <lmom>,
1406 //   - in direction <direction>.
1407 // - Else returns 0 or something in the "m_inconsistency" range.
1408 // - <lext> contains the position of farthest extension.
1409 unsigned int MPGDTrackerDigi::cExtension(double const* lpos, double const* lmom, // Input subHit
1410                                          double rT, int direction,               // Target radius
1411                                          double dZ, double startPhi,
1412                                          double endPhi, // Module parameters
1413                                          double* lext) const {
1414   unsigned int status = 0;
1415   double Mx = lpos[0], My = lpos[1], Mz = lpos[2];
1416   double Px = lmom[0], Py = lmom[1], Pz = lmom[2], norm = sqrt(Px * Px + Py * Py + Pz * Pz);
1417   // Move some distance away from <lpos>, which is expected to be sitting on
1418   // the wall of the SUBVOLUME to be ``extended''.
1419   const double margin = 10 * dd4hep::um;
1420   double t            = direction * margin / norm;
1421   Mx += t * Px;
1422   My += t * Py;
1423   Mz += t * Pz;
1424   double M2 = Mx * Mx + My * My, rIni = sqrt(M2), rLow, rUp;
1425   if (rIni < rT) {
1426     rLow = rIni;
1427     rUp  = rT;
1428   } else {
1429     rLow = rT;
1430     rUp  = rIni;
1431   }
1432   // Intersection w/ the edge in phi
1433   double tF = 0;
1434   for (double phi : {startPhi, endPhi}) {
1435     // M+t*P = 0 + t'*U. t = (My*Ux-Mx*Uy)/(Px*Uy-Py*Ux);
1436     double Ux = cos(phi), Uy = sin(phi);
1437     double D = Px * Uy - Py * Ux;
1438     if (D) { // If P not // to U
1439       double t = (My * Ux - Mx * Uy) / D;
1440       if (t * direction < 0)
1441         continue;
1442       double Ex = Mx + t * Px, Ey = My + t * Py, Ez = Mz + t * Pz;
1443       double rE = sqrt(Ex * Ex + Ey * Ey), phiE = atan2(Ey, Ex);
1444       // Note: have to discard the phi+pi solution.
1445       if (rLow < rE && rE < rUp && fabs(Ez) < dZ && fabs(phiE - phi) < 1) {
1446         status |= 0x1;
1447         tF = t;
1448       }
1449     }
1450   }
1451   // Intersection w/ the edge in Z
1452   double zLow = -dZ, zUp = +dZ;
1453   for (double Z : {zLow, zUp}) {
1454     // Mz+t*Pz = Z
1455     if (Pz) {
1456       double t = (Z - Mz) / Pz;
1457       if (t * direction < 0)
1458         continue;
1459       double Ex = Mx + t * Px, Ey = My + t * Py, rE = sqrt(Ex * Ex + Ey * Ey);
1460       double phi = atan2(Ey, Ex);
1461       if (rLow < rE && rE < rUp && startPhi < phi && phi < endPhi) {
1462         if (t < 0) {
1463           if (!status || (status && t > tF)) {
1464             status |= 0x1;
1465             tF = t;
1466           }
1467         } else if (t > 0) {
1468           if (!status || (status && t < tF)) {
1469             status |= 0x1;
1470             tF = t;
1471           }
1472         }
1473       }
1474     }
1475   }
1476   // Else intersection w/ target radius
1477   if (!status) {
1478     double a = Px * Px + Py * Py, b = Px * Mx + Py * My, c = M2 - rT * rT;
1479     if (!a) {           // P is // to Z (while it did no intersect the edge in Z)
1480       status |= 0x1000; // Inconsistency
1481     } else if (!c) {    // Hit is on target (while we've moved away from it)
1482       status |= 0x2000; // Inconsistency
1483     } else {
1484       double det = b * b - a * c;
1485       if (det >= 0) {
1486         double sqdet = sqrt(det);
1487         for (int is = 0; is < 2; is++) {
1488           int s    = 1 - 2 * is;
1489           double t = (-b + s * sqdet) / a;
1490           if (t * direction < 0)
1491             continue;
1492           double Ix = Mx + t * Px, Iy = My + t * Py, Iz = Mz + t * Pz, phi = atan2(Iy, Ix);
1493           if (fabs(Iz) > dZ || phi < startPhi || endPhi < phi)
1494             continue;
1495           if (!(status & 0x1) ||
1496               // Two intersects: let's retain the earliest one.
1497               ((status & 0x1) && fabs(t) < fabs(tF))) {
1498             tF = t;
1499             status |= 0x1;
1500           }
1501         }
1502       }
1503     }
1504   }
1505   if (status & 0x1) {
1506     lext[0] = Mx + tF * Px;
1507     lext[1] = My + tF * Py;
1508     lext[2] = Mz + tF * Pz;
1509   }
1510   return status;
1511 }
1512 unsigned int MPGDTrackerDigi::bExtension(const double* lpos, const double* lmom, // Input subHit
1513                                          double zT, int direction,               // Target Z
1514                                          double dX, double dY, // Module parameters
1515                                          double* lext) const {
1516   unsigned int status = 0;
1517   double Mx = lpos[0], My = lpos[1], Mxy[2] = {Mx, My};
1518   double Px = lmom[0], Py = lmom[1], Pxy[2] = {Px, Py};
1519   double Mz = lpos[2], Pz = lmom[2];
1520   double norm = sqrt(Px * Px + Py * Py + Pz * Pz);
1521   // Move some distance away from <lpos>, which is expected to be sitting on
1522   // the wall of the SUBVOLUME to be ``extended''.
1523   const double margin = 10 * dd4hep::um;
1524   double t            = direction * margin / norm;
1525   Mx += t * Px;
1526   My += t * Py;
1527   Mz += t * Pz;
1528   double &zIni = Mz, zLow, zUp;
1529   if (zIni < zT) {
1530     zLow = zIni;
1531     zUp  = zT;
1532   } else {
1533     zLow = zT;
1534     zUp  = zIni;
1535   }
1536   // Intersection w/ the edge in X,Y
1537   double tF       = 0;
1538   double xyLow[2] = {-dX, -dY}, xyUp[2] = {+dX, +dY};
1539   for (int xy = 0; xy < 2; xy++) {
1540     int yx       = 1 - xy;
1541     double a_Low = xyLow[xy], a_Up = xyUp[xy], Ma = Mxy[xy], Pa = Pxy[xy];
1542     double b_Low = xyLow[yx], b_Up = xyUp[yx], Mb = Mxy[yx], Pb = Pxy[yx];
1543     for (double A : {a_Low, a_Up}) {
1544       // Ma+t*Pa = A
1545       if (Pa) {
1546         double t = (A - Ma) / Pa;
1547         if (t * direction < 0)
1548           continue;
1549         double Eb = Mb + t * Pb, Ez = Mz + t * Pz;
1550         if (zLow < Ez && Ez < zUp && b_Low < Eb && Eb < b_Up) {
1551           if (!status || (status && fabs(t) < fabs(tF))) {
1552             status |= 0x1;
1553             tF = t;
1554           }
1555         }
1556       }
1557     }
1558   }
1559   // Else intersection w/ target Z
1560   if (!status) {
1561     if (Pz) {
1562       tF = (zT - Mz) / Pz;
1563       if (tF * direction > 0)
1564         status = 0x1;
1565     }
1566   }
1567   if (status) {
1568     lext[0] = Mx + tF * Px;
1569     lext[1] = My + tF * Py;
1570     lext[2] = Mz + tF * Pz;
1571   }
1572   return status;
1573 }
1574 
1575 double getRef2Cur(DetElement refVol, DetElement curVol) {
1576   // TGeoHMatrix: In order to avoid a "dangling-reference" warning,
1577   // let's take a copy of the matrix instead of a reference to it.
1578   const TGeoHMatrix toRefVol = refVol.nominal().worldTransformation();
1579   const TGeoHMatrix toCurVol = curVol.nominal().worldTransformation();
1580   const double* TRef         = toRefVol.GetTranslation();
1581   const double* TCur         = toCurVol.GetTranslation();
1582   // For some reason, it has to be "Ref-Cur", while I (Y.B) would have expected the opposite...
1583   double gdT[3];
1584   for (int i = 0; i < 3; i++)
1585     gdT[i] = TRef[i] - TCur[i];
1586   double ldT[3];
1587   toRefVol.MasterToLocalVect(gdT, ldT);
1588   return ldT[2];
1589 }
1590 
1591 std::string inconsistency(const edm4hep::EventHeader& event, unsigned int status, CellID cID,
1592                           const double* lpos, const double* lmom) {
1593   using edm4eic::unit::GeV, dd4hep::mm;
1594   return fmt::format("Event {}#{}, SimHit 0x{:016x} @ {:.2f},{:.2f},{:.2f} mm, P = "
1595                      "{:.2f},{:.2f},{:.2f} GeV inconsistency 0x{:x}",
1596                      event.getRunNumber(), event.getEventNumber(), cID, lpos[0] / mm, lpos[1] / mm,
1597                      lpos[2] / mm, lmom[0] / GeV, lmom[1] / GeV, lmom[2] / GeV, status);
1598 }
1599 std::string oddity(const edm4hep::EventHeader& event, unsigned int status, double dist, CellID cID,
1600                    const double* lpos, const double* lmom, CellID cJD, const double* lpoj,
1601                    const double* lmoj) {
1602   using edm4eic::unit::GeV, dd4hep::mm;
1603   return fmt::format("Event {}#{}, Bizarre SimHit sequence: 0x{:016x} @ {:.4f},{:.4f},{:.4f} mm, P "
1604                      "= {:.2f},{:.2f},{:.2f} GeV and 0x{:016x} @ {:.4f},{:.4f},{:.4f} mm, P = "
1605                      "{:.2f},{:.2f},{:.2f} GeV: status 0x{:x}, distance {:.4f}",
1606                      event.getRunNumber(), event.getEventNumber(), cID, lpos[0] / mm, lpos[1] / mm,
1607                      lpos[2] / mm, lmom[0] / GeV, lmom[1] / GeV, lmom[2] / GeV, cJD, lpoj[0] / mm,
1608                      lpoj[1] / mm, lpoj[2] / mm, lmoj[0] / GeV, lmoj[1] / GeV, lmoj[2] / GeV,
1609                      status, dist);
1610 }
1611 
1612 bool MPGDTrackerDigi::samePMO(const edm4hep::SimTrackerHit& sim_hit,
1613                               const edm4hep::SimTrackerHit& sim_hjt) const {
1614   // Status:
1615   // 0: Same Particle, same Module, same Origin
1616   // 0x1: Not same
1617   // Particle
1618 #if EDM4HEP_BUILD_VERSION >= EDM4HEP_VERSION(0, 99, 0)
1619   bool sameParticle = sim_hjt.getParticle() == sim_hit.getParticle();
1620 #else
1621   bool sameParticle = sim_hjt.getMCParticle() == sim_hit.getMCParticle();
1622 #endif
1623   // Module
1624   CellID vID      = sim_hit.getCellID() & m_volumeBits;
1625   CellID refID    = vID & m_moduleBits; // => the middle slice
1626   CellID vJD      = sim_hjt.getCellID() & m_volumeBits;
1627   CellID refJD    = vJD & m_moduleBits; // => the middle slice
1628   bool sameModule = refJD == refID;
1629   // Origin
1630   // Note: edm4hep::SimTrackerHit possesses an "Overlay" quality. Since I don't
1631   // know what this is, I ignore it.
1632   bool isSecondary = sim_hit.isProducedBySecondary();
1633   bool jsSecondary = sim_hjt.isProducedBySecondary();
1634   bool sameOrigin  = jsSecondary == isSecondary;
1635   return sameParticle && sameModule && sameOrigin;
1636 }
1637 
1638 double outInDistance(int shape, int orientation, double lintos[][3], double louts[][3],
1639                      double* lmom, double* lmoj) {
1640   // Outgoing/incoming distance
1641   bool ok;
1642   double lExt[3];
1643   double lmOI[3];
1644   for (int i = 0; i < 3; i++)
1645     lmOI[i] = (lmom[i] + lmoj[i]) / 2;
1646   double *lOut, *lInto;
1647   if (orientation > 0) {
1648     lOut  = louts[1];
1649     lInto = lintos[0];
1650   } else if (orientation < 0) {
1651     lOut  = louts[0];
1652     lInto = lintos[1];
1653   } else {
1654     lOut  = louts[0];
1655     lInto = lintos[0];
1656   }
1657   if (shape == 0) { // "TGeoTubeSeg"
1658     double rInto = sqrt(lInto[0] * lInto[0] + lInto[1] * lInto[1]);
1659     ok           = cExtrapolate(lOut, lmOI, rInto, lExt);
1660   } else { // "TGeoBBox"
1661     ok = bExtrapolate(lOut, lmOI, lInto[2], lExt);
1662   }
1663   if (ok) {
1664     double dist2 = 0;
1665     for (int i = 0; i < 3; i++) {
1666       double d = lExt[i] - lInto[i];
1667       dist2 += d * d;
1668     }
1669     return sqrt(dist2);
1670   } else
1671     return -1;
1672 }
1673 
1674 unsigned int MPGDTrackerDigi::extendHit(CellID refID, std::vector<std::uint64_t>& cIDs,
1675                                         int direction, double* lpini, double* lmini, double* lpend,
1676                                         double* lmend) const {
1677   unsigned int status         = 0;
1678   const VolumeManager& volman = m_detector->volumeManager();
1679   DetElement refVol           = volman.lookupDetElement(refID);
1680   const auto& shape           = refVol.solid();
1681   double *lpoE, *lmoE; // Starting position/momentum
1682   if (direction < 0) {
1683     lpoE = lpini;
1684     lmoE = lmini;
1685   } else {
1686     lpoE = lpend;
1687     lmoE = lmend;
1688   }
1689   // Let's target successively both extreme SUBVOLUMES, disregarding those
1690   // already contributing to the COALESCED hit.
1691   // => This proscribes reEntrance extension, i.e. extending past exit point
1692   //   on a path reEntering into the same SUBVOLUME.
1693   //    That option could make sense if the reEntering path is at grazing
1694   //   incidence w.r.t. SUBVOLUME inner wall. But otherwise, it leads to a
1695   //   very large difference between initial and extended hit.
1696   for (int rankE : {0, 4}) {
1697     CellID vIDE      = refID | m_stripIDs[rankE];
1698     int alreadyThere = 0;
1699     for (int i = 0; i < (int)cIDs.size(); i++) {
1700       if ((cIDs[i] & m_volumeBits) == vIDE) {
1701         alreadyThere = 1;
1702         break;
1703       }
1704     }
1705     if (alreadyThere)
1706       continue;
1707     if (std::find(cIDs.begin(), cIDs.end(), vIDE) != cIDs.end())
1708       continue;
1709     DetElement volE = volman.lookupDetElement(vIDE);
1710     double lext[3];
1711     if (std::string_view{shape.type()} == "TGeoTubeSeg") {
1712       const Tube& tExt = volE.solid();
1713       double R         = rankE == 0 ? tExt.rMin() : tExt.rMax();
1714       double startPhi  = tExt.startPhi() * radian;
1715       startPhi -= 2 * TMath::Pi();
1716       double endPhi = tExt.endPhi() * radian;
1717       endPhi -= 2 * TMath::Pi();
1718       double dZ = tExt.dZ();
1719       status    = cExtension(lpoE, lmoE, R, direction, dZ, startPhi, endPhi, lext);
1720     } else if (std::string_view{shape.type()} == "TGeoBBox") {
1721       double ref2E    = getRef2Cur(refVol, volE);
1722       const Box& bExt = volE.solid();
1723       double Z        = rankE == 0 ? -bExt.z() : +bExt.z();
1724       Z -= ref2E;
1725       double dX = bExt.x(), dY = bExt.y();
1726       status = bExtension(lpoE, lmoE, Z, direction, dX, dY, lext);
1727     } else {
1728       critical(R"(Bad input data: CellID {:x} has invalid shape "{}")", refID, shape.type());
1729       throw std::runtime_error(R"(Inconsistency: Inappropriate SimHits fed to "MPGDTrackerDigi".)");
1730     }
1731     if (status != 0x1)
1732       continue;
1733     if (direction < 0) {
1734       for (int i = 0; i < 3; i++)
1735         lpini[i] = lext[i];
1736     } else {
1737       for (int i = 0; i < 3; i++)
1738         lpend[i] = lext[i];
1739     }
1740     break;
1741   }
1742   return status;
1743 }
1744 
1745 bool MPGDTrackerDigi::denyExtension(const edm4hep::SimTrackerHit& sim_hit, double depth) const {
1746   // Non COALESCED secondary: do not extend...
1747   //  ...if in HELPER SUBVOLUME: if it is TRAVERSING, it's probably
1748   //   merely because SUBVOLUME is very thin.
1749   CellID vID          = sim_hit.getCellID() & m_volumeBits;
1750   bool isHelperVolume = m_stripRank(vID) != 0 && m_stripRank(vID) != 4;
1751   //  ...else if path length is negligible compared to potential
1752   //    extension (here, we cannot avoid using a built-in: 10%).
1753   const double fraction = .10;
1754   const double edmm = edm4eic::unit::mm, ed2dd = dd4hep::mm / edmm;
1755   bool smallPathLength = sim_hit.getPathLength() * ed2dd < fraction * depth;
1756   return isHelperVolume || smallPathLength;
1757 }
1758 
1759 void MPGDTrackerDigi::flagUnexpected(const edm4hep::EventHeader& event, int shape, double expected,
1760                                      const edm4hep::SimTrackerHit& sim_hit, double* lpini,
1761                                      double* lpend, double* lpos, double* lmom) const {
1762   //  Expectations:
1763   // I) Primary particle: position = middle of overall sensitive volume.
1764   // II) Secondary particle: no diff w.r.t. initial.
1765   //  These expectations are naive ones. When a delta ray is created w/in a
1766   // sensitive SUBVOLUME, it creates one distinct SimHit and the primary itself
1767   // no longer spans the whole SUBVOLUME nor sits at the middle. The path of
1768   // the delta ray is short, typically, but may still turn out to be extendable.
1769   //  Therefore expectations (I) and (II) are not systematically fulfilled.
1770   //  The "flagUnexpected" method is mainly there as a placeholder for a
1771   // debugging tool that would require further development.
1772   double Rnew2 = 0, Znew, diff2 = 0;
1773   for (int i = 0; i < 3; i++) {
1774     double neu = (lpini[i] + lpend[i]) / 2, alt = lpos[i];
1775     double d = neu - alt;
1776     diff2 += d * d;
1777     if (i != 2)
1778       Rnew2 += neu * neu;
1779     if (i == 2)
1780       Znew = neu;
1781   }
1782   double found = shape ? Znew : sqrt(Rnew2), residual = found - expected;
1783   bool isSecondary = sim_hit.isProducedBySecondary();
1784   bool isPrimary =
1785       !isSecondary && sqrt(lmom[0] * lmom[0] + lmom[1] * lmom[1] + lmom[2] * lmom[2]) > .1 * GeV;
1786   if ((fabs(residual) > .000001 && isPrimary) || (sqrt(diff2) > .000001 && isSecondary)) {
1787     debug("Event {}#{}, SimHit 0x{:016x} origin {:d}: d{:c} = {:.5f} diff = {:.5f}",
1788           event.getRunNumber(), event.getEventNumber(), sim_hit.getCellID(), isSecondary,
1789           shape ? 'Z' : 'R', residual, sqrt(diff2));
1790   }
1791 }
1792 
1793 } // namespace eicrecon