Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-10-23 08:28:14

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   - SIMULATION
0079     In addition to DIGITIZATION proper, the method involves a SIMULATION of
0080    some of the inner workings of the MPGD, viz. AMPLIFICATION, SIGNAL SHARING
0081    and SPREADING.
0082     + This SIMULATION relies on parameterizations obtained from beam tests.
0083     + Present version is simplistic: 2-hit clusters (except when on the edge),
0084      with identical timing and amplitude, and possibly, BELOW-THRESHOLD charge.
0085   - DIGITIZATION:
0086     + It involves the production of 2-hit clusters, called here CLUSTERIZATION.
0087     + It otherwise follows the standard steps. Remains to agree on the handling
0088      of the discrimination threshold, see Issue #1722.
0089  */
0090 
0091 #include "MPGDTrackerDigi.h"
0092 
0093 #include <DD4hep/Alignments.h>
0094 #include <DD4hep/DetElement.h>
0095 #include <DD4hep/IDDescriptor.h>
0096 #include <DD4hep/Objects.h>
0097 #include <DD4hep/Readout.h>
0098 #include <DD4hep/Shapes.h>
0099 #include <DD4hep/VolumeManager.h>
0100 #include <DD4hep/detail/SegmentationsInterna.h>
0101 #include <DDSegmentation/BitFieldCoder.h>
0102 #include <DDSegmentation/CartesianGridUV.h>
0103 #include <DDSegmentation/CartesianGridXY.h>
0104 #include <DDSegmentation/CylindricalGridPhiZ.h>
0105 #include <DDSegmentation/MultiSegmentation.h>
0106 #include <DDSegmentation/Segmentation.h>
0107 #include <Evaluator/DD4hepUnits.h>
0108 #include <Math/GenVector/Cartesian3D.h>
0109 #include <Math/GenVector/DisplacementVector3D.h>
0110 #include <Parsers/Primitives.h>
0111 #include <TGeoMatrix.h>
0112 #include <TMath.h>
0113 // Access "algorithms:GeoSvc"
0114 #include <algorithms/geo.h>
0115 #include <algorithms/logger.h>
0116 #include <edm4eic/unit_system.h>
0117 #include <edm4hep/MCParticleCollection.h>
0118 #include <edm4hep/Vector3d.h>
0119 #include <edm4hep/Vector3f.h>
0120 #include <format>
0121 #include <podio/detail/Link.h>
0122 #include <podio/detail/LinkCollectionImpl.h>
0123 #include <algorithm>
0124 #include <array>
0125 #include <cmath>
0126 #include <cstdint>
0127 #include <gsl/pointers>
0128 #include <gsl/util>
0129 #include <initializer_list>
0130 #include <iterator>
0131 #include <map>
0132 #include <memory>
0133 #include <random>
0134 #include <stdexcept>
0135 #include <tuple>
0136 #include <utility>
0137 #include <vector>
0138 
0139 #include "algorithms/digi/MPGDTrackerDigiConfig.h"
0140 
0141 using namespace dd4hep;
0142 
0143 namespace eicrecon {
0144 
0145 void MPGDTrackerDigi::init() {
0146   // Access id decoder
0147   m_detector = algorithms::GeoSvc::instance().detector();
0148 
0149   if (m_cfg.readout.empty()) {
0150     throw std::runtime_error("Readout is empty");
0151   }
0152   try {
0153     m_seg    = m_detector->readout(m_cfg.readout).segmentation();
0154     m_id_dec = m_detector->readout(m_cfg.readout).idSpec().decoder();
0155   } catch (const std::runtime_error&) {
0156     critical(R"(Failed to load ID decoder for "{}" readout.)", m_cfg.readout);
0157     throw std::runtime_error("Failed to load ID decoder");
0158   }
0159 
0160   // IDDescriptor and Segmentation
0161   parseIDDescriptor();
0162   parseSegmentation();
0163 
0164   // Ordering of SUBVOLUMES (based on "STRIP" FIELD)
0165   m_stripRank = [&](CellID vID) {
0166     CellID sID = vID & m_stripBits;
0167     for (int rank = 0; rank < 5; rank++) {
0168       if (sID == m_stripIDs[rank]) {
0169         return rank;
0170       }
0171     }
0172     return -1;
0173   };
0174   m_orientation = [&](CellID vID, CellID vJD) {
0175     int ranki = m_stripRank(vID);
0176     int rankj = m_stripRank(vJD);
0177     if (rankj > ranki) {
0178       return +1;
0179     }
0180     if (rankj < ranki) {
0181       return -1;
0182     }
0183     return 0;
0184   };
0185   m_isUpstream = [](int orientation, unsigned int status) {
0186     // Outgoing particle exits...
0187     bool isUpstream =
0188         (orientation < 0 && (status & 0x2)) ||           // ...lower wall
0189         (orientation > 0 && (status & 0x8)) ||           // ...upper wall
0190         (orientation == 0 && (status & 0x102) == 0x102); // ...lower wall and can reEnter
0191     return isUpstream;
0192   };
0193   m_isDownstream = [](int orientation, unsigned int status) {
0194     // Incoming particle enters...
0195     bool isDownstream =
0196         (orientation > 0 && (status & 0x1)) ||           // ...lower wall
0197         (orientation < 0 && (status & 0x4)) ||           // ...upper wall
0198         (orientation == 0 && (status & 0x101) == 0x101); // ...lower wall and can be reEntering
0199     return isDownstream;
0200   };
0201   // RELAXED TOLERANCE
0202   m_toleranceFactor = [](double P) {
0203     int factor = 0;
0204     if (P < 1 * dd4hep::MeV) {
0205       factor = 4;
0206     } else if (P < 10 * dd4hep::MeV) {
0207       factor = 2;
0208     } else {
0209       factor = 1;
0210     }
0211     return factor;
0212   };
0213 
0214   // CLUSTERIZATION
0215   m_binToPosition = [](FieldID bin, double cellSize, double offset) {
0216     return bin * cellSize + offset;
0217   };
0218 }
0219 
0220 // Interfaces
0221 void getLocalPosMom(const edm4hep::SimTrackerHit& sim_hit, const TGeoHMatrix& toModule,
0222                     double* lpos, double* lmom);
0223 bool cExtrapolate(const double* lpos, const double* lmom, // Input subHit
0224                   double rT,                              // Target radius
0225                   double* lext);                          // Extrapolated position @ <rT>
0226 double getRef2Cur(DetElement refVol, DetElement curVol);
0227 bool bExtrapolate(const double* lpos, const double* lmom, // Input subHit
0228                   double zT,                              // Target Z
0229                   double* lext);                          // Extrapolated position @ <zT>
0230 std::string inconsistency(const edm4hep::EventHeader& event, unsigned int status, CellID cID,
0231                           const double* lpos, const double* lmom);
0232 std::string oddity(const edm4hep::EventHeader& event, unsigned int status, double dist, CellID cID,
0233                    const double* lpos, const double* lmom, CellID cJD, const double* lpoj,
0234                    const double* lmoj);
0235 double outInDistance(int shape, int orientation, double lintos[][3], double louts[][3],
0236                      double* lmom, double* lmoj);
0237 void flagUnexpected(const edm4hep::EventHeader& event, int shape, double expected,
0238                     const edm4hep::SimTrackerHit& sim_hit, double* lpini, double* lpend,
0239                     double* lpos, double* lmom);
0240 
0241 void MPGDTrackerDigi::process(const MPGDTrackerDigi::Input& input,
0242                               const MPGDTrackerDigi::Output& output) const {
0243 
0244   const auto [headers, sim_hits]       = input;
0245   auto [raw_hits, links, associations] = output;
0246 
0247   // local random generator
0248   auto seed = m_uid.getUniqueID(*headers, name());
0249   std::default_random_engine generator(seed);
0250   std::normal_distribution<double> gaussian;
0251 
0252   // Maps of unique cellIDs with temporary structure RawHit
0253   std::map<std::uint64_t, edm4eic::MutableRawTrackerHit> cell_hit_maps[2];
0254   // A map of strip cellIDs with vector of contributing cellIDs
0255   std::map<std::uint64_t, std::vector<std::uint64_t>> stripID2cIDs;
0256   // Prepare for strip segmentation
0257   const VolumeManager& volman = m_detector->volumeManager();
0258 
0259   // Reference to event, to be used to document error messages
0260   // (N.B.: I don't know how to properly handle these "headers": may there
0261   // be more than one? none?...)
0262   const edm4hep::EventHeader& header = headers->at(0);
0263 
0264   // *************** LOOP ON sim_hits
0265   size_t sim_size = sim_hits->size();
0266   for (int idx = 0; idx < (int)sim_size; idx++) {
0267     const edm4hep::SimTrackerHit& sim_hit = sim_hits->at(idx);
0268 
0269     // ***** TIME SMEARING
0270     // - Simplistic treatment.
0271     // - A more realistic one would have to distinguish a smearing common to
0272     //  both coordinates of the 2D-strip readout (mainly due to the drifting of
0273     //  the leading primary electrons of the I+A process) from other smearing
0274     //  effects, specific to each coordinate.
0275     double time_smearing = gaussian(generator) * m_cfg.timeResolution;
0276 
0277     // ***** REFERENCE SUBVOLUME
0278     CellID refID      = sim_hit.getCellID() & m_moduleBits;
0279     DetElement refVol = volman.lookupDetElement(refID);
0280     // ***** COALESCE ALL MUTUALLY CONSISTENT SUBHITS
0281     //       EXTEND TRAVERSING SUBHITS
0282     // - Needed because we want to preserve the correlation between 'p' and
0283     //  'n' strip hits resulting from a given I+A process (which is lost when
0284     //  one accumulates hits independently based on cellID).
0285     double lpos[3], eDep, time;
0286     std::vector<std::uint64_t> cIDs;
0287     const auto& shape = refVol.solid();
0288     if (std::string_view{shape.type()} == "TGeoTubeSeg") {
0289       // ********** TUBE GEOMETRY
0290       if (!cCoalesceExtend(input, idx, cIDs, lpos, eDep, time))
0291         continue;
0292     } else if (std::string_view{shape.type()} == "TGeoBBox") {
0293       // ********** BOX GEOMETRY
0294       if (!bCoalesceExtend(input, idx, cIDs, lpos, eDep, time))
0295         continue;
0296     } else {
0297       critical(R"(Bad input data: CellID {:x} has invalid shape "{}")", refID, shape.type());
0298       throw std::runtime_error(R"(Inconsistency: Inappropriate SimHits fed to "MPGDTrackerDigi".)");
0299     }
0300 
0301     // ***** 2D-position on sensitive surface
0302     double surfPos[2];
0303     Position locPos;
0304     if (std::string_view{shape.type()} == "TGeoTubeSeg") {
0305       // Sensitive surface radius = REFERENCE VOLUME radius
0306       const Tube& tRef = refVol.solid();
0307       double R         = (tRef.rMin() + tRef.rMax()) / 2;
0308       double phi       = atan2(lpos[1], lpos[0]);
0309       surfPos[0]       = phi * R;
0310       surfPos[1]       = lpos[2];
0311       locPos           = Position(R * cos(phi), R * sin(phi), lpos[2]);
0312     } else {
0313       locPos = Position(lpos[0], lpos[1], lpos[2]);
0314       if (m_gridAngle != 0.0) { // Transform to strip frame
0315         dd4hep::DDSegmentation::Vector3D position;
0316         position.X = lpos[0];
0317         position.Y = lpos[1];
0318         position   = RotationZ(m_gridAngle) * position;
0319         surfPos[0] = position.X;
0320         surfPos[1] = position.Y;
0321       } else {
0322         surfPos[0] = lpos[0];
0323         surfPos[1] = lpos[1];
0324       }
0325     }
0326 
0327     // ********** LOOP ON p|n STRIPS
0328     for (int pn = 0; pn < 2; pn++) {
0329       // ***** CLUSTERIZATION
0330       // Cluster = (CellID, energy fraction)
0331       Cluster cluster;
0332       int status = get2HitCluster(refID, locPos, surfPos, pn, generator, cluster);
0333       if (status != 0) {
0334         CellID vIDs = (std::uint32_t)refID;
0335         CellID hIDs = refID >> 32;
0336         error(R"(SimHit (= 0x{:08x}, 0x{:08x}, {:.2f},{:.2f} cm) beyond limits of {}Strips.)", hIDs,
0337               vIDs, surfPos[0] / cm, surfPos[1] / cm, (pn != 0) ? 'n' : 'p');
0338       }
0339       // ***** DEBUGGING INFO
0340       if (level() >= algorithms::LogLevel::kDebug) {
0341         std::string sCellID = (pn != 0) ? "cellIDn" : "cellIDp";
0342         if (pn == 0) {
0343           debug("  =>=>");
0344         }
0345         for (auto clusterHit : cluster) {
0346           CellID cID = clusterHit.first;
0347           CellID hID = cID >> 32;
0348           CellID vID = (std::uint32_t)cID;
0349           debug("Hit {} = 0x{:08x}, 0x{:08x}", sCellID, hID, vID);
0350           debug("  edep = {:.0f} [eV]", clusterHit.second * eDep / eV);
0351         }
0352       }
0353       // ***** APPLY THRESHOLD / STORE (sim_hit -> stripIDs) / ACCUMULATION
0354       // (Note: Threshold is applied first. See issue #1722 in
0355       // "https://github.com/eic/EICrecon/issues/1722".)
0356       std::map<std::uint64_t, edm4eic::MutableRawTrackerHit>& cell_hit_map =
0357           gsl::at(cell_hit_maps, pn);
0358       double g = m_cfg.gain;
0359       for (auto clusterHit : cluster) {
0360         CellID cID = clusterHit.first;
0361         double f   = clusterHit.second;
0362         // Threshold
0363         // - Let's apply same threshold to the two components of the cluster.
0364         // - This, so that cluster position be preserved at Hit Reconstruction
0365         //  time (when clustering is performed).
0366         // => This has the obvious drawback of creating BELOW-THRESHOLD RawHits.
0367         if (eDep < m_cfg.threshold) {
0368           debug("  eDep {:.2f} is below threshold of {:.2f} [keV]", eDep, m_cfg.threshold / keV);
0369           continue;
0370         }
0371         stripID2cIDs[cID]   = cIDs;
0372         double result_time  = time + time_smearing;
0373         auto hit_time_stamp = (std::int32_t)(result_time * 1e3);
0374         if (!cell_hit_map.contains(cID)) {
0375           // This cell doesn't have hits
0376           cell_hit_map[cID] = {
0377               cID, (std::int32_t)std::llround(eDep * 1e6 * f * g),
0378               hit_time_stamp // ns->ps
0379           };
0380         } else {
0381           // There is previous values in the cell
0382           auto& hit = cell_hit_map[cID];
0383           debug("  Hit already exists in cell ID={}, prev. hit time: {}", cID, hit.getTimeStamp());
0384 
0385           // keep earliest time for hit
0386           hit.setTimeStamp(std::min(hit_time_stamp, hit.getTimeStamp()));
0387 
0388           // sum deposited energy
0389           auto charge = hit.getCharge();
0390           hit.setCharge(charge + (std::int32_t)std::llround(eDep * 1e6 * f * g));
0391         }
0392       }
0393     } // End loop on strip = p,n
0394   } // End loop on sim_hit's
0395 
0396   // ***** RawHit INSTANTIATION AND RawHit<-SimHits ASSOCIATION:
0397   for (auto& cell_hit_map : cell_hit_maps) {
0398     for (const auto& [stripID, hit] : cell_hit_map) {
0399       raw_hits->push_back(hit);
0400       const auto raw_hit = raw_hits->at(raw_hits->size() - 1);
0401       const auto is      = stripID2cIDs.find(stripID);
0402       if (is == stripID2cIDs.end()) {
0403         error(R"(Inconsistency: CellID {:x} not found in "stripID2cIDs" map)", stripID);
0404         throw std::runtime_error(R"(Inconsistency in the handling of "stripID2cIDs" map)");
0405       }
0406       std::vector<std::uint64_t> cIDs = is->second;
0407       for (CellID cID : cIDs) {
0408         for (const auto& sim_hit : *sim_hits) {
0409           if (sim_hit.getCellID() == cID) {
0410             // create link
0411             auto link = links->create();
0412             link.setFrom(raw_hit);
0413             link.setTo(sim_hit);
0414             link.setWeight(1.0);
0415             // set association
0416             auto hitassoc = associations->create();
0417             hitassoc.setWeight(1.0);
0418             hitassoc.setRawHit(raw_hit);
0419             hitassoc.setSimHit(sim_hit);
0420           }
0421         }
0422       }
0423     }
0424   }
0425 }
0426 
0427 //  The whole of MPGDTrackerDigi relies on a number of assumptions concerning
0428 // the structure of the MPGD detector and its subdivision into SUBVOLUMES.
0429 // These assumptions imply in turn a specific segmentation scheme. In
0430 // particular, a MultiSegmentation allowing to navigate among the SUBVOLUMES.
0431 // This MultiSegmentation, based on a "strip" discriminator, cf. IDDescriptor,
0432 // can be embedded in yet another MultiSegmentation (case of CyMBaL).
0433 //  On the other hand, the CLUSTERIZATION procedure requires accessing
0434 // segmentation parameters (pitch, offset, etc...), which in turn requires a
0435 // navigation through the multiple levels of MultiSegmentation. In order to
0436 // simplify this navigation, we decide to impose the following strict schemes:
0437 // - CyMBaL (identified by it <readout> name, viz.: "MPGDBarrelHits"):
0438 //   + MultiSegmentation discriminating on "sector",
0439 //   + MultiSegmentation discriminating on "strip",
0440 //   + CylindricalGridPhiZ, with 'p' = "phi" and 'n' = "z".
0441 // - OuterBarrel (""OuterMPGDBarrelHits"):
0442 //   + MultiSegmentation discriminating on "strip",
0443 //   + CartesianGridUV, with 'p' = "u" and 'n' = "v".
0444 // - Else:
0445 //   + MultiSegmentation discriminating on "strip",
0446 //   + CartesianGridXY, with 'p' = "x" and 'n' = "y".
0447 //  Additional restrictions:
0448 // I) Concerning parameters:
0449 //  Resolution sigma must be small enough compared to pitch that smearing does
0450 // not bring us beyond closest neighbor.
0451 // II) Concerning IDDescriptor:
0452 //  The two coordinate fields span [32,48[ and [48,64[, cf. "(in|de)crementID".
0453 // => Let's check.
0454 void MPGDTrackerDigi::parseIDDescriptor() {
0455   // Parse IDDescriptor: Retrieve CellIDs of relevant fields.
0456   // (As an illustration, here is the IDDescriptor of CyMBaL (as of 2025/11):
0457   // <id>system:8,layer:4,module:12,sensor:2,strip:28:4,phi:-16,z:-16</id>.)
0458 
0459   // - "m_volumeBits" (Volume = CellID excluding channel specification)
0460   // - "m_stripBits".
0461   // - "m_sensorStripBits"
0462   debug(R"(Parsing IDDescriptor for "{}" readout)", m_cfg.readout);
0463   CellID sensorBits = 0;
0464   for (int field = 0; field < 5; field++) {
0465     const char* fieldName = m_fieldNames[field];
0466     CellID fieldID        = 0;
0467     try {
0468       fieldID = m_id_dec->get(~((CellID)0x0), fieldName);
0469     } catch (const std::runtime_error& error) {
0470       critical(R"(No field "{}" in IDDescriptor of readout "{}".)", fieldName, m_cfg.readout);
0471       throw std::runtime_error("Invalid IDDescriptor");
0472     }
0473     const BitFieldElement& fieldElement = (*m_id_dec)[fieldName];
0474     CellID fieldBits                    = fieldID << fieldElement.offset();
0475     m_volumeBits |= fieldBits;
0476     // - "m_stripBits".
0477     if (std::string_view{fieldName} == "strip") {
0478       m_stripBits = fieldBits;
0479       // SUBVOLUMES are assigned specific bits by convention
0480       CellID bits[5] = {0x3, 0x1, 0, 0x2, 0x4};
0481       for (int subVolume = 0; subVolume < 5; subVolume++) {
0482         m_stripIDs[subVolume] = bits[subVolume] << fieldElement.offset();
0483       }
0484     }
0485     // - "m_sensorStripBits"
0486     else if (m_cfg.readout == "MPGDBarrelHits" && std::string_view{fieldName} == "sensor") {
0487       sensorBits     = fieldBits;
0488       m_sensorOffset = fieldElement.offset();
0489     }
0490   }
0491   // CellIDs derived from above
0492   m_moduleBits = m_volumeBits & ~m_stripBits;
0493   m_pStripBit  = m_stripIDs[1];
0494   m_nStripBit  = m_stripIDs[3];
0495   // Sensor|strip mask: Will serve to index the parameter map.
0496   if (m_cfg.readout == "MPGDBarrelHits") {
0497     m_sensorStripBits = sensorBits | m_stripBits;
0498   } else {
0499     m_sensorStripBits = m_stripBits;
0500   }
0501   // Get coordinate field and index.
0502   // Require coordinate fields to start @ bits 32 and 48. This is taken
0503   // advantage of by "(in|de)crementID" (and by debug messages, to cleanly
0504   // separate coordinates from the rest of CellID).
0505   int coordOffsets[2] = {64, 64};
0506   for (int pn = 0; pn < 2; pn++) {
0507     std::string coordName;
0508     if (m_cfg.readout == "MPGDBarrelHits") {
0509       coordName = pn ? "z" : "phi";
0510     } else if (m_cfg.readout == "OuterMPGDBarrelHits") {
0511       coordName = pn ? "v" : "u";
0512     } else {
0513       coordName = pn ? "y" : "x";
0514     }
0515     try {
0516       m_stripIndices[pn] = m_id_dec->index(coordName);
0517     } catch (const std::runtime_error& error) {
0518       critical(R"(No "{}" field in IDDescriptor of readout "{}".)", coordName, m_cfg.readout);
0519       throw std::runtime_error("Invalid IDDescriptor");
0520     }
0521     const BitFieldElement& fieldElement = (*m_id_dec)[coordName];
0522     int offset                          = fieldElement.offset();
0523     m_stripIncs[pn]                     = ((CellID)0x1) << offset;
0524     if (offset < coordOffsets[pn])
0525       coordOffsets[pn] = offset;
0526   }
0527   if (coordOffsets[0] != 32 || coordOffsets[1] != 48) {
0528     critical(R"(Coordinate fields in IDDescriptor of readout "{}" do not start @ bits 32 and 48.)",
0529              m_cfg.readout);
0530     throw std::runtime_error("Invalid IDDescriptor");
0531   }
0532 }
0533 void MPGDTrackerDigi::parseSegmentation() {
0534   debug(R"(Find valid "MultiSegmentation" for "{}" readout.)", m_cfg.readout);
0535 
0536   // Local function: Check restriction (I).
0537   std::function<void(int, double)> checkResolutionVsPitch = [&](int pn, double pitch) {
0538     double sigma = m_cfg.stripResolutions[pn];
0539     if (m_truncation * sigma > pitch) {
0540       critical(R"(stripResolutions[{}] (= {}) too large for pitch (={}) of "{}" readout.)", pn,
0541                sigma, pitch, m_cfg.readout);
0542       throw std::runtime_error("Space resolution parameter too large");
0543     }
0544     return;
0545   };
0546   using Segmentation               = dd4hep::DDSegmentation::Segmentation;
0547   const Segmentation* segmentation = m_seg->segmentation;
0548   // Retrieve <segmentation> parameters: pitch, offset, min, max, index.
0549   unsigned int required = 0, fulfilled = 0;
0550   if (segmentation->type() == "MultiSegmentation") {
0551     using MultiSegmentation = dd4hep::DDSegmentation::MultiSegmentation;
0552     const auto* multiSeg    = dynamic_cast<const MultiSegmentation*>(segmentation);
0553     StripParameters pars;
0554     if (m_cfg.readout == "MPGDBarrelHits") {
0555       // ********** SPECIFIC CASE: "MPGDBarrelHits"
0556       required              = 0x3 | m_pStripBit | m_nStripBit; // 0x3 sectors | stripBits
0557       unsigned int innerBit = 0x0, outerBit = 0x1;
0558       for (unsigned int sectorBit : {innerBit, outerBit}) {
0559         CellID sensorID               = ((CellID)sectorBit) << m_sensorOffset;
0560         const Segmentation& sectorSeg = multiSeg->subsegmentation(sensorID);
0561         if (multiSeg->type() != "MultiSegmentation")
0562           continue;
0563         const auto& stripMultiSeg = dynamic_cast<const MultiSegmentation&>(sectorSeg);
0564         for (CellID stripID : {m_pStripBit, m_nStripBit}) {
0565           const Segmentation& stripSeg = stripMultiSeg.subsegmentation(stripID);
0566           if (stripSeg.type() != "CylindricalGridPhiZ") {
0567             critical(
0568                 R"(Segmentation type for "{}" readout = "{}", whereas expected = "CylindricalGridPhiZ".)",
0569                 m_cfg.readout, stripSeg.type());
0570             continue;
0571           }
0572           const dd4hep::DDSegmentation::CylindricalGridPhiZ& gridPhiZ =
0573               dynamic_cast<const dd4hep::DDSegmentation::CylindricalGridPhiZ&>(stripSeg);
0574           fulfilled |= sectorBit == innerBit ? 0x1 : 0x2;
0575           fulfilled |= stripID;
0576           // Parameters -> StripParameters "pars"
0577           double radius = gridPhiZ.radius();
0578           int pn;
0579           std::string stripName;
0580           if (stripID == m_pStripBit) {
0581             pars.pitch  = gridPhiZ.gridSizePhi() * radius;
0582             pars.offset = gridPhiZ.offsetPhi() * radius;
0583             pn          = 0;
0584           } else {
0585             pars.pitch  = gridPhiZ.gridSizeZ();
0586             pars.offset = gridPhiZ.offsetZ();
0587             pn          = 1;
0588           }
0589           pars.index = m_stripIndices[pn];
0590           // Check restriction (I)
0591           checkResolutionVsPitch(pn, pars.pitch);
0592           int nStrips = m_cfg.stripNumbers[pn];
0593           // The center of the Cartesian grid is in the center of the detector, see, e.g.:
0594           //  "https://github.com/HEP-FCC/FCCDetectors/blob/main/doc/DD4hepInFCCSW.md".
0595           // I(Y.B.) recommend an offset = pitch/2, so that layout be symmetric.
0596           pars.max = (nStrips / 2 - .5) * pars.pitch + pars.offset;
0597           pars.min = (-nStrips / 2 - .5) * pars.pitch + pars.offset;
0598           // "pars" stored in "m_stripParameters" map.
0599           CellID sensorStripID             = sensorID | stripID;
0600           m_stripParameters[sensorStripID] = pars;
0601         }
0602       }
0603     } else if (m_cfg.readout == "OuterMPGDBarrelHits") {
0604       // ********** SPECIFIC CASE: "OuterMPGDBarrelHits"
0605       required = m_pStripBit | m_nStripBit;
0606       double gridAngles[2];
0607       for (unsigned int stripID : {m_pStripBit, m_nStripBit}) {
0608         const dd4hep::DDSegmentation::Segmentation& stripSeg = multiSeg->subsegmentation(stripID);
0609         if (stripSeg.type() != "CartesianGridUV") {
0610           critical(
0611               R"(Segmentation type for "{}" readout = "{}", whereas expected = "CartesianGridUV".)",
0612               m_cfg.readout, stripSeg.type());
0613           continue;
0614         }
0615         const dd4hep::DDSegmentation::CartesianGridUV& gridUV =
0616             dynamic_cast<const dd4hep::DDSegmentation::CartesianGridUV&>(stripSeg);
0617         fulfilled |= stripID;
0618         // Parameters -> StripParameters "pars"
0619         int pn;
0620         std::string stripName;
0621         if (stripID == m_pStripBit) {
0622           pars.pitch  = gridUV.gridSizeU();
0623           pars.offset = gridUV.offsetU();
0624           pn          = 0;
0625         } else {
0626           pars.pitch  = gridUV.gridSizeV();
0627           pars.offset = gridUV.offsetV();
0628           pn          = 1;
0629         }
0630         pars.index     = m_stripIndices[pn];
0631         gridAngles[pn] = gridUV.gridAngle();
0632         // Check restriction (I)
0633         checkResolutionVsPitch(pn, pars.pitch);
0634         int nStrips = m_cfg.stripNumbers[pn];
0635         pars.max    = (nStrips / 2 - .5) * pars.pitch + pars.offset;
0636         pars.min    = (-nStrips / 2 - .5) * pars.pitch + pars.offset;
0637         // "pars" stored in "m_stripParameters" map.
0638         m_stripParameters[stripID] = pars;
0639       }
0640       if (fabs(gridAngles[0] - gridAngles[1]) > 1e-6) {
0641         critical(
0642             R"(Inconsistent gridAngles of "CartesianGridUV" for readout "{}": 'p' = {:.6f}, 'n' = {:.6f}.)",
0643             m_cfg.readout, gridAngles[0], gridAngles[1]);
0644         fulfilled = 0;
0645       }
0646       m_gridAngle = gridAngles[0];
0647     } else {
0648       // ********** DEFAULT: "CartesianGridXY" Segmentation assumed
0649       required = m_pStripBit | m_nStripBit;
0650       for (unsigned int stripID : {m_pStripBit, m_nStripBit}) {
0651         const dd4hep::DDSegmentation::Segmentation& stripSeg = multiSeg->subsegmentation(stripID);
0652         if (stripSeg.type() != "CartesianGridXY") {
0653           critical(
0654               R"(Segmentation type for "{}" readout = "{}", whereas expected = "CartesianGridXY".)",
0655               m_cfg.readout, stripSeg.type());
0656           continue;
0657         }
0658         const dd4hep::DDSegmentation::CartesianGridXY& gridXY =
0659             dynamic_cast<const dd4hep::DDSegmentation::CartesianGridXY&>(stripSeg);
0660         fulfilled |= stripID;
0661         // Parameters -> StripParameters "pars"
0662         int pn;
0663         std::string stripName;
0664         if (stripID == m_pStripBit) {
0665           pars.pitch  = gridXY.gridSizeX();
0666           pars.offset = gridXY.offsetX();
0667           pn          = 0;
0668         } else {
0669           pars.pitch  = gridXY.gridSizeY();
0670           pars.offset = gridXY.offsetY();
0671           pn          = 1;
0672         }
0673         pars.index = m_stripIndices[pn];
0674         // Check restriction (I)
0675         checkResolutionVsPitch(pn, pars.pitch);
0676         int nStrips = m_cfg.stripNumbers[pn];
0677         pars.max    = (nStrips / 2 - .5) * pars.pitch + pars.offset;
0678         pars.min    = (-nStrips / 2 - .5) * pars.pitch + pars.offset;
0679         // "pars" stored in "m_stripParameters" map.
0680         m_stripParameters[stripID] = pars;
0681       }
0682     }
0683   } else {
0684     critical(
0685         R"(Segmentation type for "{}" readout = "{}", whereas expected = "MultiSegmentation".)",
0686         m_cfg.readout, segmentation->type());
0687   }
0688   if (fulfilled != required) {
0689     critical(R"(Error retrieving Segmentation parameters for "{}" readout.)", m_cfg.readout);
0690     throw std::runtime_error("Error retrieving Segmentation parameters");
0691   }
0692 }
0693 
0694 // ***** COALESCE subHits with same PMO
0695 //       EXTEND hits (subHits or coalesced hits) to full sensitive volume
0696 // - Input = Elementary subHit, specified as index into collection of SimHits.
0697 // - Output = Coalesced/extended hit, specified by:
0698 //  + list of cellIDs of elementary subHits contributing,
0699 //  + local position,
0700 //  + EDep,
0701 //  + time.
0702 //   Given that all segmentation classes foreseen for MPGDs ("CartesianGrid.."
0703 //  for Outer and EndCaps, "CylindricalGridPhiZ" for "CyMBaL") disregard the
0704 //  _global_ position argument to "dd4hep::Segmentation::cellID", we need
0705 //  the _local_ position and only that.
0706 // - Also returned: updated index.
0707 bool MPGDTrackerDigi::cCoalesceExtend(const Input& input, int& idx,
0708                                       std::vector<std::uint64_t>& cIDs, double* lpos, double& eDep,
0709                                       double& time) const {
0710   const auto [headers, sim_hits]        = input;
0711   const edm4hep::EventHeader& header    = headers->at(0);
0712   const edm4hep::SimTrackerHit& sim_hit = sim_hits->at(idx);
0713   CellID vID                            = sim_hit.getCellID() & m_volumeBits;
0714   CellID refID                          = vID & m_moduleBits; // => The REFERENCE SUBVOLUME
0715   const VolumeManager& volman           = m_detector->volumeManager();
0716   DetElement refVol                     = volman.lookupDetElement(refID);
0717   // TGeoHMatrix: In order to avoid a "dangling-reference" warning, let's take
0718   // a copy of the matrix instead of a reference to it.
0719   const TGeoHMatrix toRefVol = refVol.nominal().worldTransformation();
0720   double lmom[3];
0721   getLocalPosMom(sim_hit, toRefVol, lpos, lmom);
0722   const double edmm = edm4eic::unit::mm, ed2dd = dd4hep::mm / edmm;
0723   using dd4hep::mm;
0724   using edm4eic::unit::eV, edm4eic::unit::GeV;
0725   // Hit in progress
0726   eDep = sim_hit.getEDep();
0727   time = sim_hit.getTime();
0728   // Get VOLUME parameters
0729   const Tube& tRef = refVol.solid();
0730   double dZ        = tRef.dZ();
0731   // phi?
0732   // In "https://root.cern.ch/root/html534/guides/users-guide/Geometry.html"
0733   // TGeoTubeSeg: "phi1 is converted to [0,360] (but still expressed in
0734   // radian, as far as I can tell) and phi2 > phi1."
0735   // => Convert it to [-pi,+pi].
0736   double startPhi = tRef.startPhi() * radian;
0737   startPhi -= 2 * TMath::Pi();
0738   double endPhi = tRef.endPhi() * radian;
0739   endPhi -= 2 * TMath::Pi();
0740   // Get current SUBVOLUME
0741   DetElement curVol = volman.lookupDetElement(vID);
0742   const Tube& tCur  = curVol.solid();
0743   double rMin = tCur.rMin(), rMax = tCur.rMax();
0744   // Is TRAVERSING?
0745   double lintos[2][3], louts[2][3], lpini[3], lpend[3], lmend[3];
0746   std::copy(std::begin(lmom), std::end(lmom), std::begin(lmend));
0747   unsigned int status =
0748       cTraversing(lpos, lmom, sim_hit.getPathLength() * ed2dd, sim_hit.isProducedBySecondary(),
0749                   rMin, rMax, dZ, startPhi, endPhi, lintos, louts, lpini, lpend);
0750   if (status & m_inconsistency) { // Inconsistency => Drop current "sim_hit"
0751     error(inconsistency(header, status, sim_hit.getCellID(), lpos, lmom));
0752     return false;
0753   }
0754   cIDs.push_back(sim_hit.getCellID());
0755   std::vector<int> subHitList;
0756   if (level() >= algorithms::LogLevel::kDebug) {
0757     subHitList.push_back(idx);
0758   }
0759   // Continuations?
0760   bool isContinuation  = status & (m_intoLower | m_intoUpper);
0761   bool hasContinuation = status & (m_outLower | m_outUpper);
0762   bool canReEnter      = status & m_canReEnter;
0763   int rank             = m_stripRank(vID);
0764   if (!canReEnter) {
0765     if (rank == 0 && (status & m_intoLower))
0766       isContinuation = false;
0767     if (rank == 0 && (status & m_outLower))
0768       hasContinuation = false;
0769   }
0770   if (rank == 4 && (status & m_intoUpper))
0771     isContinuation = false;
0772   if (rank == 4 && (status & m_outUpper))
0773     hasContinuation = false;
0774   if (hasContinuation) {
0775     // ***** LOOP OVER HITS
0776     int jdx;
0777     CellID vIDPrv   = vID;
0778     size_t sim_size = sim_hits->size();
0779     for (jdx = idx + 1; jdx < (int)sim_size; jdx++) {
0780       const edm4hep::SimTrackerHit& sim_hjt = sim_hits->at(jdx);
0781       CellID vJD                            = sim_hjt.getCellID() & m_volumeBits;
0782       // Particle may start inward and re-enter, being then outward-going.
0783       // => Orientation has to be evaluated w.r.t. previous vID.
0784       int orientation = m_orientation(vIDPrv, vJD);
0785       bool isUpstream = m_isUpstream(orientation, status);
0786       bool pmoStatus  = samePMO(sim_hit, sim_hjt);
0787       if (!pmoStatus || !isUpstream) {
0788         if ((pmoStatus && !isUpstream) && !sim_hit.isProducedBySecondary()) {
0789           // Bizarre, except if it's a low energy stuff (when it then can be a
0790           // looping particle). If it's not let's flag the case, for debugging.
0791           double P = sqrt(lmom[0] * lmom[0] + lmom[1] * lmom[1] + lmom[2] * lmom[2]);
0792           if (P > 10 * dd4hep::MeV)
0793             debug(inconsistency(header, 0, sim_hit.getCellID(), lpos, lmom));
0794         }
0795         break;
0796       }
0797       // Get 'j' radii
0798       curVol           = volman.lookupDetElement(vJD);
0799       const Tube& tubj = curVol.solid();
0800       rMin             = tubj.rMin();
0801       rMax             = tubj.rMax();
0802       double lpoj[3], lmoj[3];
0803       getLocalPosMom(sim_hjt, toRefVol, lpoj, lmoj);
0804       // Is TRAVERSING through the (quasi-)common wall?
0805       double ljns[2][3], lovts[2][3], lpjni[3], lpfnd[3];
0806       status =
0807           cTraversing(lpoj, lmoj, sim_hjt.getPathLength() * ed2dd, sim_hit.isProducedBySecondary(),
0808                       rMin, rMax, dZ, startPhi, endPhi, ljns, lovts, lpjni, lpfnd);
0809       if (status & m_inconsistency) { // Inconsistency => Drop current "sim_hjt"
0810         error(inconsistency(header, status, sim_hjt.getCellID(), lpoj, lmoj));
0811         break;
0812       }
0813       // ij-Compatibility: status
0814       bool jsDownstream = m_isDownstream(orientation, status);
0815       if (!jsDownstream)
0816         break;
0817       // ij-Compatibility: close exit/entrance-distance
0818       double dist = outInDistance(0, orientation, ljns, louts, lmom, lmoj);
0819       // RELAXED TOLERANCE for low energy stuff
0820       double P          = sqrt(lmom[0] * lmom[0] + lmom[1] * lmom[1] + lmom[2] * lmom[2]);
0821       double tolerance  = m_toleranceFactor(P) * 25 * dd4hep::um;
0822       bool isCompatible = dist > 0 && dist < tolerance;
0823       if (!isCompatible) {
0824         if (!sim_hit.isProducedBySecondary())
0825           debug(oddity(header, status, dist, sim_hit.getCellID(), lpos, lmom,
0826                        /* */ sim_hjt.getCellID(), lpoj, lmoj));
0827         break;
0828       }
0829       // ***** UPDATE
0830       vIDPrv = vJD;
0831       eDep += sim_hjt.getEDep();
0832       for (int i = 0; i < 3; i++) { // Update end point position/momentum.
0833         lpend[i] = lpfnd[i];
0834         lmend[i] = lmoj[i];
0835       }
0836       // ***** BOOK-KEEPING
0837       cIDs.push_back(sim_hjt.getCellID());
0838       if (level() >= algorithms::LogLevel::kDebug) {
0839         subHitList.push_back(jdx);
0840       }
0841       // ***** CONTINUATION?
0842       hasContinuation = status & 0xa;
0843       canReEnter      = status & 0x100;
0844       if (!canReEnter && m_stripRank(vJD) == 4)
0845         hasContinuation = false;
0846       if (!hasContinuation) {
0847         jdx++;
0848         break;
0849       } else { // Update outgoing position/momentum for next iteration.
0850         for (int i = 0; i < 3; i++) {
0851           louts[0][i] = lovts[0][i];
0852           louts[1][i] = lovts[1][i];
0853         }
0854       }
0855     }
0856     idx = jdx - 1;
0857   }
0858   // ***** EXTENSION?...
0859   if (sim_hit.isProducedBySecondary() && cIDs.size() < 2)
0860     if (denyExtension(sim_hit, tCur.rMax() - tCur.rMin())) {
0861       isContinuation = hasContinuation = false;
0862     }
0863   for (int io = 0; io < 2; io++) { // ...into/out-of
0864     if ((io == 0 && !isContinuation) || (io == 1 && !hasContinuation))
0865       continue;
0866     int direction = io ? +1 : -1;
0867     extendHit(refID, cIDs, direction, lpini, lmom, lpend, lmend);
0868   }
0869   // ***** FLAG CASES W/ UNEXPECTED OUTCOME
0870   flagUnexpected(header, 0, (tRef.rMin() + tRef.rMax()) / 2, sim_hit, lpini, lpend, lpos, lmom);
0871   // ***** UPDATE (local position <lpos>, DoF)
0872   double DoF2 = 0, dir = 0;
0873   for (int i = 0; i < 3; i++) {
0874     double neu = (lpini[i] + lpend[i]) / 2, alt = lpos[i];
0875     lpos[i]  = neu;
0876     double d = neu - alt;
0877     dir += d * lmom[i];
0878     DoF2 += d * d;
0879   }
0880   // Update time by ToF from original subHit to extended/COALESCED.
0881   time += ((dir > 0) ? 1 : ((dir < 0) ? -1 : 0)) * sqrt(DoF2) / dd4hep::c_light;
0882   if (level() >= algorithms::LogLevel::kDebug) {
0883     debug("--------------------");
0884     printSubHitList(input, subHitList);
0885     debug("  =");
0886     // Print position, eDep and time of coalesced/extended hit
0887     Position locPos(lpos[0], lpos[1], lpos[2]); // Simplification: strip surface = REFERENCE surface
0888     Position globPos = refVol.nominal().localToWorld(locPos);
0889     debug("  position  = ({:7.2f},{:7.2f},{:7.2f}) [mm]", globPos.X() / mm, globPos.Y() / mm,
0890           globPos.Z() / mm);
0891     debug("  edep = {:.0f} [eV]", eDep / eV);
0892     debug("  time = {:.2f} [ns]", time);
0893   }
0894   return true;
0895 }
0896 bool MPGDTrackerDigi::bCoalesceExtend(const Input& input, int& idx,
0897                                       std::vector<std::uint64_t>& cIDs, double* lpos, double& eDep,
0898                                       double& time) const {
0899   const auto [headers, sim_hits]        = input;
0900   const edm4hep::EventHeader& header    = headers->at(0);
0901   const edm4hep::SimTrackerHit& sim_hit = sim_hits->at(idx);
0902   CellID vID                            = sim_hit.getCellID() & m_volumeBits;
0903   CellID refID                          = vID & m_moduleBits; // => The REFERENCE SUBVOLUME
0904   const VolumeManager& volman           = m_detector->volumeManager();
0905   DetElement refVol                     = volman.lookupDetElement(refID);
0906   // TGeoHMatrix: In order to avoid a "dangling-reference" warning, let's take
0907   // a copy of the matrix instead of a reference to it.
0908   const TGeoHMatrix toRefVol = refVol.nominal().worldTransformation();
0909   double lmom[3];
0910   getLocalPosMom(sim_hit, toRefVol, lpos, lmom);
0911   const double edmm = edm4eic::unit::mm, ed2dd = dd4hep::mm / edmm;
0912   using dd4hep::mm;
0913   using edm4eic::unit::eV, edm4eic::unit::GeV;
0914   // Hit in progress
0915   eDep = sim_hit.getEDep();
0916   time = sim_hit.getTime();
0917   // Get VOLUME parameters
0918   const Box& bRef = refVol.solid(); // REFERENCE SUBVOLUME
0919   double dX = bRef.x(), dY = bRef.y();
0920   // Get current SUBVOLUME
0921   DetElement curVol = volman.lookupDetElement(vID);
0922   const Box& bCur   = curVol.solid();
0923   double dZ         = bCur.z();
0924   double ref2Cur    = getRef2Cur(refVol, curVol);
0925   // Is TRAVERSING?
0926   double lintos[2][3], louts[2][3], lpini[3], lpend[3], lmend[3];
0927   std::copy(std::begin(lmom), std::end(lmom), std::begin(lmend));
0928   unsigned int status =
0929       bTraversing(lpos, lmom, ref2Cur, sim_hit.getPathLength() * ed2dd,
0930                   sim_hit.isProducedBySecondary(), dZ, dX, dY, lintos, louts, lpini, lpend);
0931   if (status & m_inconsistency) { // Inconsistency => Drop current "sim_hit"
0932     error(inconsistency(header, status, sim_hit.getCellID(), lpos, lmom));
0933     return false;
0934   }
0935   cIDs.push_back(sim_hit.getCellID());
0936   std::vector<int> subHitList;
0937   if (level() >= algorithms::LogLevel::kDebug) {
0938     subHitList.push_back(idx);
0939   }
0940   // Continuations?
0941   int rank             = m_stripRank(vID);
0942   bool isContinuation  = status & (m_intoLower | m_intoUpper);
0943   bool hasContinuation = status & (m_outLower | m_outUpper);
0944   if ((rank == 0 && (status & m_intoLower)) || (rank == 4 && (status & m_intoUpper)))
0945     isContinuation = false;
0946   if ((rank == 0 && (status & m_outLower)) || (rank == 4 && (status & m_outUpper)))
0947     hasContinuation = false;
0948   if (hasContinuation) {
0949     // ***** LOOP OVER SUBHITS
0950     int jdx;
0951     CellID vIDPrv   = vID;
0952     size_t sim_size = sim_hits->size();
0953     for (jdx = idx + 1; jdx < (int)sim_size; jdx++) {
0954       const edm4hep::SimTrackerHit& sim_hjt = sim_hits->at(jdx);
0955       CellID vJD                            = sim_hjt.getCellID() & m_volumeBits;
0956       int orientation                       = m_orientation(vIDPrv, vJD);
0957       bool isUpstream                       = m_isUpstream(orientation, status);
0958       bool pmoStatus                        = samePMO(sim_hit, sim_hjt);
0959       if (!pmoStatus || !isUpstream) {
0960         if ((pmoStatus && !isUpstream) && !sim_hit.isProducedBySecondary()) {
0961           // Bizarre: let's flag the case for debugging, if not low energy.
0962           double P = sqrt(lmom[0] * lmom[0] + lmom[1] * lmom[1] + lmom[2] * lmom[2]);
0963           if (P > 10 * dd4hep::MeV)
0964             debug(inconsistency(header, 0, sim_hit.getCellID(), lpos, lmom));
0965         }
0966         break;
0967       }
0968       // Get 'j' Z
0969       curVol          = volman.lookupDetElement(vJD); // 'j' SUBVOLUME
0970       const Box& boxj = curVol.solid();
0971       dZ              = boxj.z();
0972       double ref2j    = getRef2Cur(refVol, curVol);
0973       // Is TRAVERSING through the (quasi)-common border?
0974       double lpoj[3], lmoj[3];
0975       getLocalPosMom(sim_hjt, toRefVol, lpoj, lmoj);
0976       double ljns[2][3], lovts[2][3], lpjni[3], lpfnd[3];
0977       status = bTraversing(lpoj, lmoj, ref2j, sim_hjt.getPathLength() * ed2dd,
0978                            sim_hit.isProducedBySecondary(), dZ, dX, dY, ljns, lovts, lpjni, lpfnd);
0979       if (status & m_inconsistency) { // Inconsistency => Drop current "sim_hjt"
0980         error(inconsistency(header, status, sim_hjt.getCellID(), lpoj, lmoj));
0981         break;
0982       }
0983       // ij-Compatibility: status
0984       bool jsDownstream = m_isDownstream(orientation, status);
0985       if (!jsDownstream)
0986         break;
0987       // ij-Compatibility: close exit/entrance-distance
0988       double dist = outInDistance(1, orientation, ljns, louts, lmom, lmoj);
0989       // RELAXED TOLERANCE for low energy stuff
0990       double P          = sqrt(lmom[0] * lmom[0] + lmom[1] * lmom[1] + lmom[2] * lmom[2]);
0991       double tolerance  = m_toleranceFactor(P) * 25 * dd4hep::um;
0992       bool isCompatible = dist > 0 && dist < tolerance;
0993       if (!isCompatible) {
0994         if (!sim_hit.isProducedBySecondary())
0995           debug(oddity(header, status, dist, sim_hit.getCellID(), lpos, lmom,
0996                        /* */ sim_hjt.getCellID(), lpoj, lmoj));
0997         break;
0998       }
0999       // ***** UPDATE
1000       vIDPrv = vJD;
1001       eDep += sim_hjt.getEDep();
1002       for (int i = 0; i < 3; i++) { // Update end point position/momentum.
1003         lpend[i] = lpfnd[i];
1004         lmend[i] = lmoj[i];
1005       }
1006       // ***** BOOK-KEEPING
1007       cIDs.push_back(sim_hjt.getCellID());
1008       if (level() >= algorithms::LogLevel::kDebug) {
1009         subHitList.push_back(jdx);
1010       }
1011       // ***** CONTINUATION?
1012       hasContinuation = status & 0xa;
1013       if (!hasContinuation) {
1014         jdx++;
1015         break;
1016       } else { // Update outgoing position/momentum for next iteration.
1017         for (int i = 0; i < 3; i++) {
1018           louts[0][i] = lovts[0][i];
1019           louts[1][i] = lovts[1][i];
1020         }
1021       }
1022     }
1023     idx = jdx - 1;
1024   }
1025   // ***** EXTENSION?...
1026   if (sim_hit.isProducedBySecondary() && cIDs.size() < 2)
1027     if (denyExtension(sim_hit, bCur.z())) {
1028       isContinuation = hasContinuation = false;
1029     }
1030   for (int io = 0; io < 2; io++) { // ...into/out-of
1031     if ((io == 0 && !isContinuation) || (io == 1 && !hasContinuation))
1032       continue;
1033     int direction = io ? +1 : -1;
1034     extendHit(refID, cIDs, direction, lpini, lmom, lpend, lmend);
1035   }
1036   // ***** FLAG CASES W/ UNEXPECTED OUTCOME
1037   flagUnexpected(header, 1, 0, sim_hit, lpini, lpend, lpos, lmom);
1038   // ***** UPDATE (local position <lpos>, DoF)
1039   double DoF2 = 0, dir = 0;
1040   for (int i = 0; i < 3; i++) {
1041     double neu = (lpini[i] + lpend[i]) / 2, alt = lpos[i];
1042     lpos[i]  = neu;
1043     double d = neu - alt;
1044     dir += d * lmom[i];
1045     DoF2 += d * d;
1046   }
1047   // Update time by ToF from original subHit to extended/COALESCED.
1048   time += ((dir > 0) ? 1 : ((dir < 0) ? -1 : 0)) * sqrt(DoF2) / dd4hep::c_light;
1049   if (level() >= algorithms::LogLevel::kDebug) {
1050     debug("--------------------");
1051     printSubHitList(input, subHitList);
1052     debug("  =");
1053     // Print position, eDep and time of coalesced/extended hit
1054     Position locPos(lpos[0], lpos[1], lpos[2]); // Simplification: strip surface = REFERENCE surface
1055     Position globPos = refVol.nominal().localToWorld(locPos);
1056     debug("  position  = ({:7.2f},{:7.2f},{:7.2f}) [mm]", globPos.X() / mm, globPos.Y() / mm,
1057           globPos.Z() / mm);
1058     debug("  edep = {:.0f} [eV]", eDep / eV);
1059     debug("  time = {:.2f} [ns]", time);
1060   }
1061   return true;
1062 }
1063 void MPGDTrackerDigi::printSubHitList(const Input& input, std::vector<int>& subHitList) const {
1064   const auto [headers, sim_hits] = input;
1065   const double edmm              = edm4eic::unit::mm;
1066   using edm4eic::unit::eV, edm4eic::unit::GeV;
1067   int ldx = 0;
1068   for (int kdx : subHitList) {
1069     const edm4hep::SimTrackerHit& sim_hp = sim_hits->at(kdx);
1070     CellID cIDk                          = sim_hp.getCellID();
1071     CellID hIDk = cIDk >> 32, vIDk = cIDk & m_volumeBits;
1072     if (ldx == 0) {
1073       debug("Hit cellID{:d} = 0x{:08x}, 0x{:08x}", ldx++, hIDk, vIDk);
1074     } else {
1075       debug("  + cellID{:d} = 0x{:08x}, 0x{:08x}", ldx++, hIDk, vIDk);
1076     }
1077     debug("  position  = ({:7.2f},{:7.2f},{:7.2f}) [mm]", sim_hp.getPosition().x / edmm,
1078           sim_hp.getPosition().y / edmm, sim_hp.getPosition().z / edmm);
1079     debug("  xy_radius = {:.2f}",
1080           std::hypot(sim_hp.getPosition().x, sim_hp.getPosition().y) / edmm);
1081     debug("  momentum  = ({:.2f}, {:.2f}, {:.2f}) [GeV]", sim_hp.getMomentum().x / GeV,
1082           sim_hp.getMomentum().y / GeV, sim_hp.getMomentum().z / GeV);
1083     debug("  edep = {:.0f} [eV]", sim_hp.getEDep() / eV);
1084     debug("  time = {:.2f} [ns]", sim_hp.getTime());
1085   }
1086 }
1087 
1088 void getLocalPosMom(const edm4hep::SimTrackerHit& sim_hit, const TGeoHMatrix& toModule,
1089                     double* lpos, double* lmom) {
1090   const edm4hep::Vector3d& pos = sim_hit.getPosition();
1091   // Length: Inputs are in EDM4eic units. Let's move to DD4hep units.
1092   const double edmm = edm4eic::unit::mm, ed2dd = dd4hep::mm / edmm;
1093   const double gpos[3]         = {pos.x * ed2dd, pos.y * ed2dd, pos.z * ed2dd};
1094   const edm4hep::Vector3f& mom = sim_hit.getMomentum();
1095   const double gmom[3]         = {mom.x, mom.y, mom.z};
1096   toModule.MasterToLocal(gpos, lpos);
1097   toModule.MasterToLocalVect(gmom, lmom);
1098 }
1099 
1100 // ******************** TRAVERSING?
1101 // Particle can be born/dead (then its position is not (entrance+exit)/2).
1102 // Or it can exit through the edge.
1103 // - Returned Status code, see header.
1104 //     Also, for internal use:
1105 //     0x10: Enters through edge
1106 //     0x20: Exits  through edge
1107 // - <lintos>/<louts>: Positions @ lower/upper wall upon Enter-/Exit-ing (when endorsed by <status>)
1108 // - <lpini>/<lpend>: Positions of extrema
1109 // - TOLERANCE? For MIPs, a tolerance of 1 µM works fine. But for lower energy,
1110 //  looks like we need something somewhat larger. The ideal would be to base
1111 //  the value on Molière width. Here, I use a somewhat arbitrary built-in.
1112 //  - If particle found to reach wall, w/in tolerance, assign end points to
1113 //   walls (instead of <lpos>+/-path/2). It will make so that the eventual
1114 //   extrapolated position falls exactly at mid-plane, even in the case of a
1115 //   low energy particle, where path may be affected by multiscattering. This,
1116 //   provided that particle is not a secondary.
1117 unsigned int MPGDTrackerDigi::cTraversing(const double* lpos, const double* lmom, double path,
1118                                           bool isSecondary,         // Input subHit
1119                                           double rMin, double rMax, // Current instance of SUBVOLUME
1120                                           double dZ, double startPhi,
1121                                           double endPhi, // Module parameters
1122                                           double lintos[][3], double louts[][3], double* lpini,
1123                                           double* lpend) const {
1124   unsigned int status = 0;
1125   double Mx = lpos[0], My = lpos[1], Mz = lpos[2], M2 = Mx * Mx + My * My;
1126   double Px = lmom[0], Py = lmom[1], Pz = lmom[2];
1127   // Intersection w/ the edge in phi
1128   double tIn = 0, tOut = 0;
1129   for (double phi : {startPhi, endPhi}) {
1130     // M+t*P = 0 + t'*U. t = (My*Ux-Mx*Uy)/(Px*Uy-Py*Ux);
1131     double Ux = cos(phi), Uy = sin(phi);
1132     double D = Px * Uy - Py * Ux;
1133     if (D) { // If P not // to U
1134       double t  = (My * Ux - Mx * Uy) / D;
1135       double Ex = Mx + t * Px, Ey = My + t * Py, Ez = Mz + t * Pz;
1136       double rE = sqrt(Ex * Ex + Ey * Ey), phiE = atan2(Ey, Ex);
1137       // The above does not distinguish between phi and phi+pi.
1138       // => Have to explicitly discard the latter.
1139       if (rMin < rE && rE < rMax && fabs(Ez) < dZ && fabs(phiE - phi) < 1) {
1140         if (t < 0) {
1141           status |= 0x10;
1142           tIn = t;
1143         } else {
1144           status |= 0x20;
1145           tOut = t;
1146         }
1147       }
1148     }
1149   }
1150   // Intersection w/ the edge in Z
1151   double zLow = -dZ, zUp = +dZ;
1152   for (double Z : {zLow, zUp}) {
1153     // Mz+t*Pz = Z
1154     if (Pz) {
1155       double t  = (Z - Mz) / Pz;
1156       double Ex = Mx + t * Px, Ey = My + t * Py, rE = sqrt(Ex * Ex + Ey * Ey);
1157       double phi = atan2(Ey, Ex);
1158       if (rMin < rE && rE < rMax && startPhi < phi && phi < endPhi) {
1159         if (t < 0) {
1160           if (!(status & 0x10) || ((status & 0x10) && t > tIn)) {
1161             status |= 0x10;
1162             tIn = t;
1163           }
1164         } else if (t > 0) {
1165           if (!(status & 0x20) || ((status & 0x20) && t < tOut)) {
1166             status |= 0x20;
1167             tOut = t;
1168           }
1169         }
1170       }
1171     }
1172   }
1173   // Intersection w/ tube walls
1174   double ts[3 /* rMin/rMax/edge */][2 /* In/Out */] = {
1175       {0, 0}, {0, 0}, {tIn, tOut}}; // Up to two intersections
1176   double a = Px * Px + Py * Py, b = Px * Mx + Py * My;
1177   for (int lu = 0; lu < 2; lu++) { // rMin/rMax
1178     double R;
1179     unsigned int statGene;
1180     if (lu == 1) {
1181       R        = rMax;
1182       statGene = 0x4;
1183     } else {
1184       R        = rMin;
1185       statGene = 0x1;
1186     }
1187     double c = M2 - R * R;
1188     if (!a) { // P is // to Z. Yet no intersect w/ Z edge.
1189       if ((status & 0x30) != 0x30)
1190         status |= 0x1000;
1191       continue;      // Inconsistency
1192     } else if (!c) { // Hit is on wall: inconsistency.
1193       status |= 0x2000;
1194       continue;
1195     } else {
1196       double det = b * b - a * c;
1197       if (det < 0) {
1198         if (lu == 1) { // No intersection w/ outer wall: inconsistency.
1199           status |= 0x4000;
1200           continue;
1201         }
1202       } else {
1203         double sqdet = sqrt(det);
1204         for (int is = 0; is < 2; is++) {
1205           int s     = 1 - 2 * is;
1206           double t  = (-b + s * sqdet) / a;
1207           double Ix = Mx + t * Px, Iy = My + t * Py, Iz = Mz + t * Pz, phi = atan2(Iy, Ix);
1208           if (fabs(Iz) > dZ || phi < startPhi || endPhi < phi)
1209             continue;
1210           if (t < 0) {
1211             // Two rMin intersects in same back/forward direction may happen
1212             // (one and and only one of them may then be hidden by edge).
1213             // => Have to allow wall intersect to coexist w/ edge intersect.
1214             //   This only for rMin, but for simplicity's sake...
1215             if (status & statGene) { // Two <0 ts: can only happen when rMin
1216               double tPrv = ts[lu][0];
1217               if (t > tPrv) {
1218                 ts[lu][0] = t;
1219                 ts[lu][1] = tPrv; // Current is actually IN, previous is OUT despite being <0
1220               }
1221               status |= statGene << 1;
1222             } else {
1223               ts[lu][0] = t;
1224               status |= statGene;
1225             }
1226           } else {                        // (if t > 0)
1227             if (status & statGene << 1) { // Two >0 ts: can only happen when rMin
1228               double tPrv = ts[lu][1];
1229               if (t < tPrv) {
1230                 ts[lu][1] = t;
1231                 ts[lu][0] = tPrv; // Current is actually OUT, previous is IN despite being >0
1232               }
1233               status |= statGene;
1234             } else {
1235               ts[lu][1] = t;
1236               status |= statGene << 1;
1237             }
1238           }
1239         }
1240       }
1241     }
1242   }
1243   // Combine w/ edge in/out, based on "t".
1244   // - A priori, wall crossing (conditioned by w/in edges) and edge crossing (
1245   //  conditioned by rMin<rE<rMax) are mutually exclusive...
1246   // - ...This, except when particle re-enters through the lower wall...
1247   // =>
1248   //  - No reEntrance: Let's double-check that the above holds, canceling wall
1249   //   crossing and raising an inconsistency status bit when not.
1250   //  - Else:
1251   //    - If doesReEnter, reEntrance is a mere transient trip outside the
1252   //     SUBVOLUME. => Cancel it.
1253   //    - Else disregard edge, possibly raising an inconsistency status bit.
1254   // Hit lies outside?
1255   // - ...Or when hit position lies outside volume (it can happen, thanks to
1256   //  limited precision).
1257   //  => If this is the case, let's swap their exit vs. entrance status. This
1258   //  will prevent inconsistencies from showing up below.
1259   // Can reEnter: does reEnter?
1260   bool canReEnter = (status & 0x3) == 0x3;
1261   double norm     = sqrt(a + Pz * Pz);
1262   if (canReEnter) {
1263     bool doesReEnter = true;
1264     for (int i12 = 0; i12 < 2; i12++) {
1265       double t               = ts[0][i12];
1266       int s                  = t > 0 ? +1 : -1;
1267       const double tolerance = 20 * dd4hep::um;
1268       if (path / 2 - s * t * norm < tolerance)
1269         doesReEnter = false;
1270     }
1271     if (doesReEnter) {
1272       status &= ~0x3;
1273       canReEnter = false;
1274     }
1275   }
1276   double rHit = sqrt(M2);
1277   if (rHit < rMin && !canReEnter) {
1278     unsigned int statvs = status;
1279     if (statvs & 0x1) {
1280       status &= ~0x1;
1281       status |= 0x2;
1282       ts[0][1] = -ts[0][0];
1283     }
1284     if (statvs & 0x2) {
1285       status &= ~0x2;
1286       status |= 0x1;
1287       ts[0][0] = -ts[0][1];
1288     }
1289   } else if (rHit > rMax) {
1290     unsigned int statvs = status;
1291     if (statvs & 0x4) {
1292       status &= ~0x4;
1293       status |= 0x8;
1294       ts[1][1] = -ts[1][0];
1295     }
1296     if (statvs & 0x8) {
1297       status &= ~0x8;
1298       status |= 0x4;
1299       ts[1][0] = -ts[1][1];
1300     }
1301   }
1302   for (int lu = 0; lu < 2; lu++) { // rMin/rMax
1303     unsigned int statGene = lu ? 0x4 : 0x1;
1304     if (status & statGene) {
1305       double t = ts[lu][0];
1306       if (t < 0) {
1307         if (status & 0x10) {
1308           if (lu == 1 || !canReEnter) { // No reEntrance:
1309             status |= 0x10000;          //   Inconsistency
1310             status &= ~statGene;        //   Cancel wall crossing
1311           } else {                      // ReEntrance: disregard edge crossing
1312             if (t < tIn)
1313               status |= 0x10000; // Inconsistency
1314           }
1315         }
1316       } else { // if (t > 0)
1317         if (status & 0x20) {
1318           if (lu == 1 || !canReEnter) {
1319             status |= 0x20000;
1320             status &= ~statGene;
1321           } else {
1322             if (t > tOut)
1323               status |= 0x20000;
1324           }
1325         }
1326       }
1327     }
1328     if (status & statGene << 1) {
1329       double t = ts[lu][1];
1330       if (t < 0) {
1331         if (status & 0x10) {
1332           if (lu == 1 || !canReEnter) {
1333             status |= 0x40000;
1334             status &= ~(statGene << 1);
1335           } else {
1336             if (t < tIn)
1337               status |= 0x40000;
1338           }
1339         }
1340       } else { // if (t > 0)
1341         if (status & 0x20) {
1342           if (lu == 1 || !canReEnter) {
1343             status |= 0x80000;
1344             status &= ~(statGene << 1);
1345           } else {
1346             if (t > tOut)
1347               status |= 0x80000;
1348           }
1349         }
1350       }
1351     }
1352   }
1353   // Is particle born/dead prior to entering/exiting?
1354   // - sim_hit must have been assigned the mean position: (entrance+exit)/2
1355   // - Let's then check entrance/exit against sim_hit's position +/- path/2.
1356   //   When the latter is too short, it means that the particle firing the
1357   //  hit gets born or dies in the SUBVOLUME ("dying" taken here in the broad
1358   //  sense of undergoing a discrete physics process).
1359   // => We remove the corresponding bit in the <status> pattern.
1360   // - Note that we not only require that the path be long enough, but also
1361   //  that it matches exactly distances to entrance/exit.
1362   if (canReEnter)
1363     status |= 0x100; // Remember that particle can re-enter.
1364   double at           = path / 2 / norm;
1365   unsigned int statws = 0;
1366   for (int is = 0; is < 2; is++) {
1367     int s     = 1 - 2 * is;
1368     double Ix = s * at * Px, Iy = s * at * Py, Iz = s * at * Pz;
1369     for (int lu = 0; lu < 2; lu++) { // Lower/upper wall
1370       unsigned int statvs = lu ? 0x4 : 0x1;
1371       for (int io = 0; io < 2; io++) {
1372         statvs <<= io;
1373         if (status & statvs) {
1374           double t = ts[lu][io];
1375           if (t * s < 0)
1376             continue;
1377           double dIx = t * Px - Ix, dIy = t * Py - Iy, dIz = t * Pz - Iz;
1378           double dist = sqrt(dIx * dIx + dIy * dIy + dIz * dIz);
1379           // RELAXED TOLERANCE for low energy stuff
1380           double tolerance = m_toleranceFactor(norm) * 20 * dd4hep::um;
1381           if (dist < tolerance)
1382             statws |= statvs;
1383         }
1384       }
1385     }
1386   }
1387   if (!(statws & 0x5)) /* No entrance */
1388     status &= ~0x5;
1389   if (!(statws & 0xa)) /* No exit */
1390     status &= ~0xa;
1391   // ***** End points
1392   // Assign end points to walls, if not a secondary and provided it's not a
1393   // reEntrance case, which case is more difficult to handle and we leave aside.
1394   if (((status & 0x5) == 0x1 || (status & 0x5) == 0x4) && !isSecondary) {
1395     double tIn = (status & 0x1) ? ts[0][0] : ts[1][0];
1396     lpini[0]   = Mx + tIn * Px;
1397     lpini[1]   = My + tIn * Py;
1398     lpini[2]   = Mz + tIn * Pz;
1399   } else {
1400     lpini[0] = Mx - at * Px;
1401     lpini[1] = My - at * Py;
1402     lpini[2] = Mz - at * Pz;
1403   }
1404   if (((status & 0xa) == 0x2 || (status & 0xa) == 0x8) && !isSecondary) {
1405     double tOut = (status & 0x2) ? ts[0][1] : ts[1][1];
1406     lpend[0]    = Mx + tOut * Px;
1407     lpend[1]    = My + tOut * Py;
1408     lpend[2]    = Mz + tOut * Pz;
1409   } else {
1410     lpend[0] = Mx + at * Px;
1411     lpend[1] = My + at * Py;
1412     lpend[2] = Mz + at * Pz;
1413   }
1414   // End points when on the walls
1415   for (int lu = 0; lu < 2; lu++) {
1416     unsigned int statvs = lu ? 0x4 : 0x1;
1417     double tIn = ts[lu][0], tOut = ts[lu][1];
1418     if (status & statvs) {
1419       lintos[lu][0] = Mx + tIn * Px;
1420       lintos[lu][1] = My + tIn * Py;
1421       lintos[lu][2] = Mz + tIn * Pz;
1422     }
1423     statvs <<= 1;
1424     if (status & statvs) {
1425       louts[lu][0] = Mx + tOut * Px;
1426       louts[lu][1] = My + tOut * Py;
1427       louts[lu][2] = Mz + tOut * Pz;
1428     }
1429   }
1430   return status;
1431 }
1432 unsigned int MPGDTrackerDigi::bTraversing(const double* lpos, const double* lmom, double ref2Cur,
1433                                           double path,
1434                                           bool isSecondary,     // Input subHit
1435                                           double dZ,            // Current instance of SUBVOLUME
1436                                           double dX, double dY, // Module parameters
1437                                           double lintos[][3], double louts[][3], double* lpini,
1438                                           double* lpend) const {
1439   unsigned int status = 0;
1440   double Mx = lpos[0], My = lpos[1], Mxy[2] = {Mx, My};
1441   double Px = lmom[0], Py = lmom[1], Pxy[2] = {Px, Py};
1442   double Mz = lpos[2] + ref2Cur, Pz = lmom[2];
1443   // Intersection w/ the edge in X,Y
1444   double tIn = 0, tOut = 0;
1445   double xyLow[2] = {-dX, -dY}, xyUp[2] = {+dX, +dY};
1446   for (int xy = 0; xy < 2; xy++) {
1447     int yx       = 1 - xy;
1448     double a_Low = xyLow[xy], a_Up = xyUp[xy], Ma = Mxy[xy], Pa = Pxy[xy];
1449     double b_Low = xyLow[yx], b_Up = xyUp[yx], Mb = Mxy[yx], Pb = Pxy[yx];
1450     for (double A : {a_Low, a_Up}) {
1451       // Ma+t*Pa = A
1452       if (Pa) {
1453         double t  = (A - Ma) / Pa;
1454         double Eb = Mb + t * Pb, Ez = Mz + t * Pz;
1455         if (b_Low < Eb && Eb < b_Up && fabs(Ez) < dZ) {
1456           if (t < 0) {
1457             if (!(status & 0x10) || ((status & 0x10) && t > tIn)) {
1458               status |= 0x10;
1459               tIn = t;
1460             }
1461           } else if (t > 0) {
1462             if (!(status & 0x20) || ((status & 0x20) && t < tOut)) {
1463               status |= 0x20;
1464               tOut = t;
1465             }
1466           }
1467         }
1468       }
1469     }
1470   }
1471   // Intersection w/ box walls
1472   for (int lu = 0; lu < 2; lu++) {
1473     int s                 = 2 * lu - 1;
1474     double Z              = s * dZ;
1475     unsigned int statGene = lu ? 0x4 : 0x1;
1476     // Mz+t*Pz = Z
1477     if (Pz) {
1478       double t = (Z - Mz) / Pz;
1479       if (t < 0) {
1480         if (!(status & 0x10) || ((status & 0x10) && t > tIn)) {
1481           status |= statGene;
1482           tIn = t;
1483         }
1484       } else if (t > 0) {
1485         if (!(status & 0x20) || ((status & 0x20) && t < tOut)) {
1486           status |= statGene << 1;
1487           tOut = t;
1488         }
1489       }
1490     }
1491   }
1492   // Is particle born/dead prior to entering/exiting?
1493   // - sim_hit must have been assigned the mean position: (entrance+exit)/2
1494   // - Let's then check entrance/exit against sim_hit's position +/- path/2.
1495   //   When the latter is too short, it means that the particle firing the
1496   //  hit gets born or dies in the SUBVOLUME ("dying" taken here in the broad
1497   //  sense of undergoing a discrete physics process).
1498   // => We remove the corresponding bit in the <status> pattern.
1499   // - Note that we not only require that the path be long enough, but also
1500   //  that it matches exactly distances to entrance/exit.
1501   double norm = sqrt(Px * Px + Py * Py + Pz * Pz), at = path / 2 / norm;
1502   unsigned int statws = 0;
1503   for (int is = 0; is < 2; is++) {
1504     int s     = 1 - 2 * is;
1505     double Ix = s * at * Px, Iy = s * at * Py, Iz = s * at * Pz;
1506     for (int lu = 0; lu < 2; lu++) { // Lower/upper wall
1507       unsigned int statvs = lu ? 0x4 : 0x1;
1508       for (int io = 0; io < 2; io++) {
1509         statvs <<= io;
1510         if (status & statvs) {
1511           double t = io ? tOut : tIn;
1512           if (t * s < 0)
1513             continue;
1514           double dIx = t * Px - Ix, dIy = t * Py - Iy, dIz = t * Pz - Iz;
1515           double dist = sqrt(dIx * dIx + dIy * dIy + dIz * dIz);
1516           // RELAXED TOLERANCE for low energy stuff
1517           double tolerance = m_toleranceFactor(norm) * 20 * dd4hep::um;
1518           if (dist < tolerance)
1519             statws |= statvs;
1520         }
1521       }
1522     }
1523   }
1524   if (!(statws & 0x5)) /* No entrance */
1525     status &= ~0x5;
1526   if (!(statws & 0xa)) /* No exit */
1527     status &= ~0xa;
1528   // ***** OUTPUT POSITIONS
1529   Mz -= ref2Cur; // Go back to REFERENCE SUBVOLUME
1530   // End points:
1531   // Assign end points to walls, if not a secondary.
1532   if ((status & 0x5) && !isSecondary) {
1533     lpini[0] = Mx + tIn * Px;
1534     lpini[1] = My + tIn * Py;
1535     lpini[2] = Mz + tIn * Pz;
1536   } else {
1537     lpini[0] = Mx - at * Px;
1538     lpini[1] = My - at * Py;
1539     lpini[2] = Mz - at * Pz;
1540   }
1541   if ((status & 0xa) && !isSecondary) {
1542     lpend[0] = Mx + tOut * Px;
1543     lpend[1] = My + tOut * Py;
1544     lpend[2] = Mz + tOut * Pz;
1545   } else {
1546     lpend[0] = Mx + at * Px;
1547     lpend[1] = My + at * Py;
1548     lpend[2] = Mz + at * Pz;
1549   }
1550   // End points when on the walls:
1551   for (int lu = 0; lu < 2; lu++) {
1552     unsigned int statvs = lu ? 0x4 : 0x1;
1553     if (status & statvs) {
1554       lintos[lu][0] = Mx + tIn * Px;
1555       lintos[lu][1] = My + tIn * Py;
1556       lintos[lu][2] = Mz + tIn * Pz;
1557     }
1558     statvs <<= 1;
1559     if (status & statvs) {
1560       louts[lu][0] = Mx + tOut * Px;
1561       louts[lu][1] = My + tOut * Py;
1562       louts[lu][2] = Mz + tOut * Pz;
1563     }
1564   }
1565   return status;
1566 }
1567 
1568 // ***** EXTRAPOLATE
1569 bool cExtrapolate(const double* lpos, const double* lmom, // Input subHit
1570                   double rT,                              // Target radius
1571                   double* lext)                           // Extrapolated position @ <rT>
1572 {
1573   bool ok   = false;
1574   double Mx = lpos[0], My = lpos[1], Mz = lpos[2], M2 = Mx * Mx + My * My;
1575   double Px = lmom[0], Py = lmom[1], Pz = lmom[2];
1576   double a = Px * Px + Py * Py, b = Px * Mx + Py * My, c = M2 - rT * rT;
1577   double tF = 0;
1578   if (!c)
1579     ok = true;
1580   else if (a) { // P is not // to Z
1581     double det = b * b - a * c;
1582     if (det >= 0) {
1583       double sqdet = sqrt(det);
1584       for (int is = 0; is < 2; is++) {
1585         int s    = 1 - 2 * is;
1586         double t = (-b + s * sqdet) / a, norm = sqrt(a + Pz * Pz);
1587         // "t" may happen to be slightly <0, because of limited precision
1588         if (t * norm < -dd4hep::nm)
1589           continue;
1590         if (!ok ||
1591             // Two intersects: let's retain the earliest one.
1592             (ok && fabs(t) < fabs(tF))) {
1593           tF = t;
1594           ok = true;
1595         }
1596       }
1597     }
1598   }
1599   if (ok) {
1600     lext[0] = Mx + tF * Px;
1601     lext[1] = My + tF * Py;
1602     lext[2] = Mz + tF * Pz;
1603   }
1604   return ok;
1605 }
1606 bool bExtrapolate(const double* lpos, const double* lmom, // Input subHit
1607                   double zT,                              // Target Z
1608                   double* lext)                           // Extrapolated position @ <zT>
1609 {
1610   bool ok   = false;
1611   double Mx = lpos[0], My = lpos[1], Mz = lpos[2];
1612   double Px = lmom[0], Py = lmom[1], Pz = lmom[2], norm = sqrt(Px * Px + Py * Py + Pz * Pz);
1613   double tF = 0;
1614   if (Pz) {
1615     tF = (zT - Mz) / Pz;
1616     // "t" may happen to be slightly <0, because of limited precision
1617     ok = tF * norm > -dd4hep::nm;
1618   }
1619   if (ok) {
1620     lext[0] = Mx + tF * Px;
1621     lext[1] = My + tF * Py;
1622     lext[2] = Mz + tF * Pz;
1623   }
1624   return ok;
1625 }
1626 
1627 // ***** EXTENSION
1628 // At variance to EXTRAPOLATION, we take edges (phi,Z/X,Y) into account.
1629 // - Returns 0x1 if
1630 //   - there is an extrapolation between position <lpos> and target,
1631 //   - within edge limits,
1632 //   - along momentum <lmom>,
1633 //   - in direction <direction>.
1634 // - Else returns 0 or something in the "m_inconsistency" range.
1635 // - <lext> contains the position of farthest extension.
1636 unsigned int MPGDTrackerDigi::cExtension(double const* lpos, double const* lmom, // Input subHit
1637                                          double rT, int direction,               // Target radius
1638                                          double dZ, double startPhi,
1639                                          double endPhi, // Module parameters
1640                                          double* lext) const {
1641   unsigned int status = 0;
1642   double Mx = lpos[0], My = lpos[1], Mz = lpos[2];
1643   double Px = lmom[0], Py = lmom[1], Pz = lmom[2], norm = sqrt(Px * Px + Py * Py + Pz * Pz);
1644   // Move some distance away from <lpos>, which is expected to be sitting on
1645   // the wall of the SUBVOLUME to be ``extended''.
1646   const double margin = 10 * dd4hep::um;
1647   double t            = direction * margin / norm;
1648   Mx += t * Px;
1649   My += t * Py;
1650   Mz += t * Pz;
1651   double M2 = Mx * Mx + My * My, rIni = sqrt(M2), rLow, rUp;
1652   if (rIni < rT) {
1653     rLow = rIni;
1654     rUp  = rT;
1655   } else {
1656     rLow = rT;
1657     rUp  = rIni;
1658   }
1659   // Intersection w/ the edge in phi
1660   double tF = 0;
1661   for (double phi : {startPhi, endPhi}) {
1662     // M+t*P = 0 + t'*U. t = (My*Ux-Mx*Uy)/(Px*Uy-Py*Ux);
1663     double Ux = cos(phi), Uy = sin(phi);
1664     double D = Px * Uy - Py * Ux;
1665     if (D) { // If P not // to U
1666       double t = (My * Ux - Mx * Uy) / D;
1667       if (t * direction < 0)
1668         continue;
1669       double Ex = Mx + t * Px, Ey = My + t * Py, Ez = Mz + t * Pz;
1670       double rE = sqrt(Ex * Ex + Ey * Ey), phiE = atan2(Ey, Ex);
1671       // Note: have to discard the phi+pi solution.
1672       if (rLow < rE && rE < rUp && fabs(Ez) < dZ && fabs(phiE - phi) < 1) {
1673         status |= 0x1;
1674         tF = t;
1675       }
1676     }
1677   }
1678   // Intersection w/ the edge in Z
1679   double zLow = -dZ, zUp = +dZ;
1680   for (double Z : {zLow, zUp}) {
1681     // Mz+t*Pz = Z
1682     if (Pz) {
1683       double t = (Z - Mz) / Pz;
1684       if (t * direction < 0)
1685         continue;
1686       double Ex = Mx + t * Px, Ey = My + t * Py, rE = sqrt(Ex * Ex + Ey * Ey);
1687       double phi = atan2(Ey, Ex);
1688       if (rLow < rE && rE < rUp && startPhi < phi && phi < endPhi) {
1689         if (t < 0) {
1690           if (!status || (status && t > tF)) {
1691             status |= 0x1;
1692             tF = t;
1693           }
1694         } else if (t > 0) {
1695           if (!status || (status && t < tF)) {
1696             status |= 0x1;
1697             tF = t;
1698           }
1699         }
1700       }
1701     }
1702   }
1703   // Else intersection w/ target radius
1704   if (!status) {
1705     double a = Px * Px + Py * Py, b = Px * Mx + Py * My, c = M2 - rT * rT;
1706     if (!a) {           // P is // to Z (while it did no intersect the edge in Z)
1707       status |= 0x1000; // Inconsistency
1708     } else if (!c) {    // Hit is on target (while we've moved away from it)
1709       status |= 0x2000; // Inconsistency
1710     } else {
1711       double det = b * b - a * c;
1712       if (det >= 0) {
1713         double sqdet = sqrt(det);
1714         for (int is = 0; is < 2; is++) {
1715           int s    = 1 - 2 * is;
1716           double t = (-b + s * sqdet) / a;
1717           if (t * direction < 0)
1718             continue;
1719           double Ix = Mx + t * Px, Iy = My + t * Py, Iz = Mz + t * Pz, phi = atan2(Iy, Ix);
1720           if (fabs(Iz) > dZ || phi < startPhi || endPhi < phi)
1721             continue;
1722           if (!(status & 0x1) ||
1723               // Two intersects: let's retain the earliest one.
1724               ((status & 0x1) && fabs(t) < fabs(tF))) {
1725             tF = t;
1726             status |= 0x1;
1727           }
1728         }
1729       }
1730     }
1731   }
1732   if (status & 0x1) {
1733     lext[0] = Mx + tF * Px;
1734     lext[1] = My + tF * Py;
1735     lext[2] = Mz + tF * Pz;
1736   }
1737   return status;
1738 }
1739 unsigned int MPGDTrackerDigi::bExtension(const double* lpos, const double* lmom, // Input subHit
1740                                          double zT, int direction,               // Target Z
1741                                          double dX, double dY, // Module parameters
1742                                          double* lext) const {
1743   unsigned int status = 0;
1744   double Mx = lpos[0], My = lpos[1], Mxy[2] = {Mx, My};
1745   double Px = lmom[0], Py = lmom[1], Pxy[2] = {Px, Py};
1746   double Mz = lpos[2], Pz = lmom[2];
1747   double norm = sqrt(Px * Px + Py * Py + Pz * Pz);
1748   // Move some distance away from <lpos>, which is expected to be sitting on
1749   // the wall of the SUBVOLUME to be ``extended''.
1750   const double margin = 10 * dd4hep::um;
1751   double t            = direction * margin / norm;
1752   Mx += t * Px;
1753   My += t * Py;
1754   Mz += t * Pz;
1755   double &zIni = Mz, zLow, zUp;
1756   if (zIni < zT) {
1757     zLow = zIni;
1758     zUp  = zT;
1759   } else {
1760     zLow = zT;
1761     zUp  = zIni;
1762   }
1763   // Intersection w/ the edge in X,Y
1764   double tF       = 0;
1765   double xyLow[2] = {-dX, -dY}, xyUp[2] = {+dX, +dY};
1766   for (int xy = 0; xy < 2; xy++) {
1767     int yx       = 1 - xy;
1768     double a_Low = xyLow[xy], a_Up = xyUp[xy], Ma = Mxy[xy], Pa = Pxy[xy];
1769     double b_Low = xyLow[yx], b_Up = xyUp[yx], Mb = Mxy[yx], Pb = Pxy[yx];
1770     for (double A : {a_Low, a_Up}) {
1771       // Ma+t*Pa = A
1772       if (Pa) {
1773         double t = (A - Ma) / Pa;
1774         if (t * direction < 0)
1775           continue;
1776         double Eb = Mb + t * Pb, Ez = Mz + t * Pz;
1777         if (zLow < Ez && Ez < zUp && b_Low < Eb && Eb < b_Up) {
1778           if (!status || (status && fabs(t) < fabs(tF))) {
1779             status |= 0x1;
1780             tF = t;
1781           }
1782         }
1783       }
1784     }
1785   }
1786   // Else intersection w/ target Z
1787   if (!status) {
1788     if (Pz) {
1789       tF = (zT - Mz) / Pz;
1790       if (tF * direction > 0)
1791         status = 0x1;
1792     }
1793   }
1794   if (status) {
1795     lext[0] = Mx + tF * Px;
1796     lext[1] = My + tF * Py;
1797     lext[2] = Mz + tF * Pz;
1798   }
1799   return status;
1800 }
1801 
1802 double getRef2Cur(DetElement refVol, DetElement curVol) {
1803   // TGeoHMatrix: In order to avoid a "dangling-reference" warning,
1804   // let's take a copy of the matrix instead of a reference to it.
1805   const TGeoHMatrix toRefVol = refVol.nominal().worldTransformation();
1806   const TGeoHMatrix toCurVol = curVol.nominal().worldTransformation();
1807   const double* TRef         = toRefVol.GetTranslation();
1808   const double* TCur         = toCurVol.GetTranslation();
1809   // For some reason, it has to be "Ref-Cur", while I (Y.B) would have expected the opposite...
1810   double gdT[3];
1811   for (int i = 0; i < 3; i++)
1812     gdT[i] = TRef[i] - TCur[i];
1813   double ldT[3];
1814   toRefVol.MasterToLocalVect(gdT, ldT);
1815   return ldT[2];
1816 }
1817 
1818 std::string inconsistency(const edm4hep::EventHeader& event, unsigned int status, CellID cID,
1819                           const double* lpos, const double* lmom) {
1820   using edm4eic::unit::GeV, dd4hep::mm;
1821   return std::format("Event {}#{}, SimHit 0x{:016x} @ {:.2f},{:.2f},{:.2f} mm, P = "
1822                      "{:.2f},{:.2f},{:.2f} GeV inconsistency 0x{:x}",
1823                      event.getRunNumber(), event.getEventNumber(), cID, lpos[0] / mm, lpos[1] / mm,
1824                      lpos[2] / mm, lmom[0] / GeV, lmom[1] / GeV, lmom[2] / GeV, status);
1825 }
1826 std::string oddity(const edm4hep::EventHeader& event, unsigned int status, double dist, CellID cID,
1827                    const double* lpos, const double* lmom, CellID cJD, const double* lpoj,
1828                    const double* lmoj) {
1829   using edm4eic::unit::GeV, dd4hep::mm;
1830   return std::format("Event {}#{}, Bizarre SimHit sequence: 0x{:016x} @ {:.4f},{:.4f},{:.4f} mm, P "
1831                      "= {:.2f},{:.2f},{:.2f} GeV and 0x{:016x} @ {:.4f},{:.4f},{:.4f} mm, P = "
1832                      "{:.2f},{:.2f},{:.2f} GeV: status 0x{:x}, distance {:.4f}",
1833                      event.getRunNumber(), event.getEventNumber(), cID, lpos[0] / mm, lpos[1] / mm,
1834                      lpos[2] / mm, lmom[0] / GeV, lmom[1] / GeV, lmom[2] / GeV, cJD, lpoj[0] / mm,
1835                      lpoj[1] / mm, lpoj[2] / mm, lmoj[0] / GeV, lmoj[1] / GeV, lmoj[2] / GeV,
1836                      status, dist);
1837 }
1838 
1839 bool MPGDTrackerDigi::samePMO(const edm4hep::SimTrackerHit& sim_hit,
1840                               const edm4hep::SimTrackerHit& sim_hjt) const {
1841   // Status:
1842   // 0: Same Particle, same Module, same Origin
1843   // 0x1: Not same
1844   // Particle
1845   bool sameParticle = sim_hjt.getParticle() == sim_hit.getParticle();
1846   // Module
1847   CellID vID      = sim_hit.getCellID() & m_volumeBits;
1848   CellID refID    = vID & m_moduleBits; // => the middle slice
1849   CellID vJD      = sim_hjt.getCellID() & m_volumeBits;
1850   CellID refJD    = vJD & m_moduleBits; // => the middle slice
1851   bool sameModule = refJD == refID;
1852   // Origin
1853   // Note: edm4hep::SimTrackerHit possesses an "Overlay" quality. Since I don't
1854   // know what this is, I ignore it.
1855   bool isSecondary = sim_hit.isProducedBySecondary();
1856   bool jsSecondary = sim_hjt.isProducedBySecondary();
1857   bool sameOrigin  = jsSecondary == isSecondary;
1858   return sameParticle && sameModule && sameOrigin;
1859 }
1860 
1861 double outInDistance(int shape, int orientation, double lintos[][3], double louts[][3],
1862                      double* lmom, double* lmoj) {
1863   // Outgoing/incoming distance
1864   bool ok;
1865   double lExt[3];
1866   double lmOI[3];
1867   for (int i = 0; i < 3; i++)
1868     lmOI[i] = (lmom[i] + lmoj[i]) / 2;
1869   double *lOut, *lInto;
1870   if (orientation > 0) {
1871     lOut  = louts[1];
1872     lInto = lintos[0];
1873   } else if (orientation < 0) {
1874     lOut  = louts[0];
1875     lInto = lintos[1];
1876   } else {
1877     lOut  = louts[0];
1878     lInto = lintos[0];
1879   }
1880   if (shape == 0) { // "TGeoTubeSeg"
1881     double rInto = sqrt(lInto[0] * lInto[0] + lInto[1] * lInto[1]);
1882     ok           = cExtrapolate(lOut, lmOI, rInto, lExt);
1883   } else { // "TGeoBBox"
1884     ok = bExtrapolate(lOut, lmOI, lInto[2], lExt);
1885   }
1886   if (ok) {
1887     double dist2 = 0;
1888     for (int i = 0; i < 3; i++) {
1889       double d = lExt[i] - lInto[i];
1890       dist2 += d * d;
1891     }
1892     return sqrt(dist2);
1893   } else
1894     return -1;
1895 }
1896 
1897 unsigned int MPGDTrackerDigi::extendHit(CellID refID, std::vector<std::uint64_t>& cIDs,
1898                                         int direction, double* lpini, double* lmini, double* lpend,
1899                                         double* lmend) const {
1900   unsigned int status         = 0;
1901   const VolumeManager& volman = m_detector->volumeManager();
1902   DetElement refVol           = volman.lookupDetElement(refID);
1903   const auto& shape           = refVol.solid();
1904   double *lpoE, *lmoE; // Starting position/momentum
1905   if (direction < 0) {
1906     lpoE = lpini;
1907     lmoE = lmini;
1908   } else {
1909     lpoE = lpend;
1910     lmoE = lmend;
1911   }
1912   // Let's target successively both extreme SUBVOLUMES, disregarding those
1913   // already contributing to the COALESCED hit.
1914   // => This proscribes reEntrance extension, i.e. extending past exit point
1915   //   on a path reEntering into the same SUBVOLUME.
1916   //    That option could make sense if the reEntering path is at grazing
1917   //   incidence w.r.t. SUBVOLUME inner wall. But otherwise, it leads to a
1918   //   very large difference between initial and extended hit.
1919   for (int rankE : {0, 4}) {
1920     CellID vIDE      = refID | m_stripIDs[rankE];
1921     int alreadyThere = 0;
1922     for (int i = 0; i < (int)cIDs.size(); i++) {
1923       if ((cIDs[i] & m_volumeBits) == vIDE) {
1924         alreadyThere = 1;
1925         break;
1926       }
1927     }
1928     if (alreadyThere)
1929       continue;
1930     if (std::find(cIDs.begin(), cIDs.end(), vIDE) != cIDs.end())
1931       continue;
1932     DetElement volE = volman.lookupDetElement(vIDE);
1933     double lext[3];
1934     if (std::string_view{shape.type()} == "TGeoTubeSeg") {
1935       const Tube& tExt = volE.solid();
1936       double R         = rankE == 0 ? tExt.rMin() : tExt.rMax();
1937       double startPhi  = tExt.startPhi() * radian;
1938       startPhi -= 2 * TMath::Pi();
1939       double endPhi = tExt.endPhi() * radian;
1940       endPhi -= 2 * TMath::Pi();
1941       double dZ = tExt.dZ();
1942       status    = cExtension(lpoE, lmoE, R, direction, dZ, startPhi, endPhi, lext);
1943     } else if (std::string_view{shape.type()} == "TGeoBBox") {
1944       double ref2E    = getRef2Cur(refVol, volE);
1945       const Box& bExt = volE.solid();
1946       double Z        = rankE == 0 ? -bExt.z() : +bExt.z();
1947       Z -= ref2E;
1948       double dX = bExt.x(), dY = bExt.y();
1949       status = bExtension(lpoE, lmoE, Z, direction, dX, dY, lext);
1950     } else {
1951       critical(R"(Bad input data: CellID {:x} has invalid shape "{}")", refID, shape.type());
1952       throw std::runtime_error(R"(Inconsistency: Inappropriate SimHits fed to "MPGDTrackerDigi".)");
1953     }
1954     if (status != 0x1)
1955       continue;
1956     if (direction < 0) {
1957       for (int i = 0; i < 3; i++)
1958         lpini[i] = lext[i];
1959     } else {
1960       for (int i = 0; i < 3; i++)
1961         lpend[i] = lext[i];
1962     }
1963     break;
1964   }
1965   return status;
1966 }
1967 
1968 bool MPGDTrackerDigi::denyExtension(const edm4hep::SimTrackerHit& sim_hit, double depth) const {
1969   // Non COALESCED secondary: do not extend...
1970   //  ...if in HELPER SUBVOLUME: if it is TRAVERSING, it's probably
1971   //   merely because SUBVOLUME is very thin.
1972   CellID vID          = sim_hit.getCellID() & m_volumeBits;
1973   bool isHelperVolume = m_stripRank(vID) != 0 && m_stripRank(vID) != 4;
1974   //  ...else if path length is negligible compared to potential
1975   //    extension (here, we cannot avoid using a built-in: 10%).
1976   const double fraction = .10;
1977   const double edmm = edm4eic::unit::mm, ed2dd = dd4hep::mm / edmm;
1978   bool smallPathLength = sim_hit.getPathLength() * ed2dd < fraction * depth;
1979   return isHelperVolume || smallPathLength;
1980 }
1981 
1982 void MPGDTrackerDigi::flagUnexpected(const edm4hep::EventHeader& event, int shape, double expected,
1983                                      const edm4hep::SimTrackerHit& sim_hit, double* lpini,
1984                                      double* lpend, double* lpos, double* lmom) const {
1985   //  Expectations:
1986   // I) Primary particle: position = middle of overall sensitive volume.
1987   // II) Secondary particle: no diff w.r.t. initial.
1988   //  These expectations are naive ones. When a delta ray is created w/in a
1989   // sensitive SUBVOLUME, it creates one distinct SimHit and the primary itself
1990   // no longer spans the whole SUBVOLUME nor sits at the middle. The path of
1991   // the delta ray is short, typically, but may still turn out to be extendable.
1992   //  Therefore expectations (I) and (II) are not systematically fulfilled.
1993   //  The "flagUnexpected" method is mainly there as a placeholder for a
1994   // debugging tool that would require further development.
1995   double Rnew2 = 0, Znew, diff2 = 0;
1996   for (int i = 0; i < 3; i++) {
1997     double neu = (lpini[i] + lpend[i]) / 2, alt = lpos[i];
1998     double d = neu - alt;
1999     diff2 += d * d;
2000     if (i != 2)
2001       Rnew2 += neu * neu;
2002     if (i == 2)
2003       Znew = neu;
2004   }
2005   double found = shape ? Znew : sqrt(Rnew2), residual = found - expected;
2006   bool isSecondary = sim_hit.isProducedBySecondary();
2007   bool isPrimary =
2008       !isSecondary && sqrt(lmom[0] * lmom[0] + lmom[1] * lmom[1] + lmom[2] * lmom[2]) > .1 * GeV;
2009   if ((fabs(residual) > .000001 && isPrimary) || (sqrt(diff2) > .000001 && isSecondary)) {
2010     debug("Event {}#{}, SimHit 0x{:016x} origin {:d}: d{:c} = {:.5f} diff = {:.5f}",
2011           event.getRunNumber(), event.getEventNumber(), sim_hit.getCellID(), isSecondary,
2012           shape ? 'Z' : 'R', residual, sqrt(diff2));
2013   }
2014 }
2015 
2016 // ***** CLUSTERIZATION
2017 // Returns:
2018 // 0: OK
2019 // 1: input hit is beyond limits
2020 int MPGDTrackerDigi::get2HitCluster(CellID refID,
2021                                     Position& locPos,  // In DD4hep frame
2022                                     double surfPos[2], // In Surface frame
2023                                     int pn,            // 'p' or 'n' strip
2024                                     std::default_random_engine& generator, Cluster& cluster) const {
2025   //Sim2IDs sim2IDs;
2026   // Master CellID, from "locPos"
2027   CellID stripID = m_stripIDs[pn ? 3 : 1]; // 'p' is 2nd in line, 'n' is 4th.
2028   const Position dummy(0, 0, 0);
2029   CellID masterID = m_seg->cellID(locPos, dummy, refID | stripID);
2030   // Retrieve StripParameters (for current sensor, current strip)
2031   const StripParameters* pars;
2032   CellID sensorStripID = masterID & m_sensorStripBits;
2033   try {
2034     pars = &m_stripParameters.at(sensorStripID);
2035   } catch (const std::out_of_range& oor) {
2036     critical(R"(Error retrieving StripParameters for readout "{}", cellID 0x{:0>16x}: {}.)",
2037              m_cfg.readout, masterID, oor.what());
2038     throw std::runtime_error("Error retrieving StripParameters");
2039   }
2040   // Abscissa = coordinate along measurement axis.
2041   // hA = simHit   Abscissa (or abscissa of input hit)
2042   // mA = master   Abscissa (or abscissa of strip fired by input hit)
2043   // sA = smeared  Abscissa
2044   // nA = neighbor Abscissa
2045   const double& sigma = m_cfg.stripResolutions[pn];
2046   const double min = pars->min, max = pars->max, pitch = pars->pitch;
2047   double hA = surfPos[pn];
2048   if (hA < min - (m_truncation - .1 * dd4hep::cm) * sigma ||
2049       hA > max + (m_truncation - .1 * dd4hep::cm) * sigma) {
2050     // Exclude hits beyond limits.
2051     // - In standard situations, min/max are extrema extremorum.
2052     // - Here we allow for a little more (.1 cm), to cope width whatever
2053     //  non-standard case.
2054     // - The limit not to exceed in any case is when the excess would prevent
2055     //  the _truncated_ Gaussian smearing from moving us back w/in [min,max].
2056     return 1;
2057   }
2058   FieldID stripNum = m_id_dec->get(masterID, pars->index);
2059   double mA        = m_binToPosition(stripNum, pitch, pars->offset);
2060   std::normal_distribution<double> gaussian;
2061   auto stripGauss = [&](double abscissa, double sigma, double min, double max) {
2062     // Truncated Gaussian: +/-n*sigmas or readout extrema
2063     double low = abscissa - m_truncation * sigma;
2064     double up  = abscissa + m_truncation * sigma;
2065     if (min > low)
2066       low = min;
2067     if (max < up)
2068       up = max;
2069     double x;
2070     do {
2071       x = abscissa + gaussian(generator) * sigma;
2072     } while (x < low || up < x);
2073     return x;
2074   };
2075   double sA = stripGauss(hA, sigma, min, max);
2076   // ***** CLUSTER
2077   // Are we on the edge? Edge being extreme half-cell .
2078   if (sA < min + pitch / 2 || sA > max - pitch / 2) {
2079     // If indeed, single-hit cluster
2080     cluster.push_back({masterID, 1});
2081   } else {
2082     // Else two-hit cluster
2083     CellID neighID, inc = m_stripIncs[pn];
2084     double nA;
2085     // (In|de)crement neighbor w.r.t. master:
2086     // - Simple addition works well in most cases...
2087     // - ...But not when the overall coordinate field is =0 and the pCoordinate
2088     //  is decremented: we then get, overall, 0xffffffff, i.e. -1 (if decrement
2089     //  is 1, that is), nor when vice versa, pCoordinate of 0xffff (=-1) is
2090     //  incremented.
2091     // - In practice, the pCoordinate is the only one affected. Yet, to be on
2092     //  the safe side, let's ensure the spectator coordinate (i.e. "1-pn") is
2093     //  left unchanged for both (p|n)Coordinates.
2094     auto incrementID = [&](int pn) {
2095       CellID spectatorBits = pn == 0 ? ((CellID)0xffff) << 48 : ((CellID)0xffff) << 32;
2096       CellID iniID         = masterID & spectatorBits;
2097       neighID              = masterID + inc;
2098       neighID &= ~spectatorBits;
2099       neighID |= iniID;
2100     };
2101     auto decrementID = [&](int pn) {
2102       CellID spectatorBits = pn == 0 ? ((CellID)0xffff) << 48 : ((CellID)0xffff) << 32;
2103       CellID iniID         = masterID & spectatorBits;
2104       neighID              = masterID - inc;
2105       neighID &= ~spectatorBits;
2106       neighID |= iniID;
2107     };
2108     if (sA < mA) {
2109       decrementID(pn);
2110       nA = mA - pitch;
2111     } else {
2112       incrementID(pn);
2113       nA = mA + pitch;
2114     }
2115     // Amplitude Faction (Note: It can't be but >0, see "init").
2116     double fn = (sA - mA) / (nA - mA);
2117     cluster.push_back({masterID, 1 - fn});
2118     cluster.push_back({neighID, fn});
2119   }
2120   return 0;
2121 }
2122 
2123 } // namespace eicrecon