Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-07-03 07:52:35

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