Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-07-31 08:22:52

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