Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-11-06 09:32:53

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