Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-06-17 07:50:57

0001 // SPDX-License-Identifier: LGPL-3.0-or-later
0002 // Copyright (C) 2026 ePIC Collaboration
0003 
0004 #include <DD4hep/Detector.h>
0005 #include <DD4hep/IDDescriptor.h>
0006 #include <DD4hep/Readout.h>
0007 #include <DDSegmentation/BitFieldCoder.h>
0008 #include <algorithms/geo.h>
0009 #include <catch2/catch_approx.hpp>
0010 #include <catch2/catch_test_macros.hpp>
0011 #include <edm4eic/EDM4eicVersion.h>
0012 #include <edm4eic/MCRecoTrackerHitAssociationCollection.h>
0013 #include <edm4eic/RawTrackerHitCollection.h>
0014 #include <edm4hep/EventHeaderCollection.h>
0015 #include <edm4hep/MCParticleCollection.h>
0016 #include <edm4hep/SimTrackerHitCollection.h>
0017 #include <edm4hep/Vector3d.h>
0018 #include <edm4hep/Vector3f.h>
0019 #include <podio/detail/Link.h>
0020 #include <array>
0021 #include <cstdlib>
0022 #include <deque>
0023 #include <gsl/pointers>
0024 #include <string>
0025 
0026 #if EDM4EIC_BUILD_VERSION >= EDM4EIC_VERSION(8, 7, 0)
0027 #include <edm4eic/MCRecoTrackerHitLinkCollection.h>
0028 #endif
0029 
0030 #include "algorithms/digi/MPGDTrackerDigi.h"
0031 #include "algorithms/digi/MPGDTrackerDigiConfig.h"
0032 
0033 using eicrecon::MPGDTrackerDigi;
0034 using eicrecon::MPGDTrackerDigiConfig;
0035 
0036 // Helper: build a CellID from field values using the IDDescriptor encoder.
0037 // system=3 matches the mock geometry's addPhysVolID("system", 3).
0038 // strip=1 → p-strip, strip=2 → n-strip
0039 // sensor=0 (default, not used for non-CyMBaL detectors)
0040 static dd4hep::DDSegmentation::CellID makeCellID(const dd4hep::IDDescriptor& desc, int system,
0041                                                  int layer, int module, int sensor, int strip,
0042                                                  int x, int y) {
0043   auto encoder                       = desc.decoder();
0044   dd4hep::DDSegmentation::CellID cid = 0;
0045   encoder->set(cid, "system", system);
0046   encoder->set(cid, "layer", layer);
0047   encoder->set(cid, "module", module);
0048   encoder->set(cid, "sensor", sensor);
0049   encoder->set(cid, "strip", strip);
0050   encoder->set(cid, "x", x);
0051   encoder->set(cid, "y", y);
0052   return cid;
0053 }
0054 
0055 static dd4hep::IDDescriptor getMPGDIdDesc() {
0056   return algorithms::GeoSvc::instance().detector()->readout("MockMPGDHits").idSpec();
0057 }
0058 
0059 // Helper: create a default MPGDTrackerDigiConfig for the mock geometry.
0060 // The mock geometry uses 1 mm pitch CartesianGridXY segmentation.
0061 // stripResolutions must be small relative to pitch (restriction I in the algorithm).
0062 static MPGDTrackerDigiConfig makeDefaultConfig() {
0063   MPGDTrackerDigiConfig cfg;
0064   cfg.readout          = "MockMPGDHits";
0065   cfg.gain             = 10000;
0066   cfg.stripResolutions = {0.15, 0.15}; // 150 um in DD4hep units (mm)
0067   cfg.stripNumbers     = {1024, 1024};
0068   cfg.threshold        = 0.0; // No threshold
0069   cfg.timeResolution   = 8.0;
0070   return cfg;
0071 }
0072 
0073 // Helper: create a SimTrackerHit positioned in the mock MPGD geometry.
0074 // The hit must have a valid CellID with strip field set (for VolumeManager lookup),
0075 // a position in global coordinates consistent with the mock geometry placement,
0076 // momentum for traversal direction, and nonzero pathLength.
0077 // The mock sensor is a Box at z ~ ±0.025 inside module at z = imod*0.5.
0078 // Module is inside envelope placed at world origin with system=3.
0079 static void createSimHit(edm4hep::SimTrackerHitCollection& sim_hits,
0080                          edm4hep::MCParticleCollection& mc_particles,
0081                          dd4hep::DDSegmentation::CellID cellID, double global_x, double global_y,
0082                          double global_z, double momX, double momY, double momZ, double eDep,
0083                          double time, double pathLength) {
0084   auto particle = mc_particles.create();
0085   particle.setPDG(11); // electron
0086   particle.setMass(0.000511f);
0087   particle.setCharge(-1.0f);
0088   particle.setGeneratorStatus(1);
0089 
0090   auto hit = sim_hits.create();
0091   hit.setCellID(cellID);
0092   // SimTrackerHit positions are in EDM4hep units (mm)
0093   hit.setPosition({global_x, global_y, global_z});
0094   hit.setMomentum({static_cast<float>(momX), static_cast<float>(momY), static_cast<float>(momZ)});
0095   hit.setEDep(eDep);
0096   hit.setTime(time);
0097   hit.setPathLength(pathLength);
0098   hit.setParticle(particle);
0099 }
0100 
0101 TEST_CASE("MPGDTrackerDigi: empty input produces empty output", "[MPGDTrackerDigi]") {
0102   MPGDTrackerDigi algo("test_digi_empty");
0103   auto cfg = makeDefaultConfig();
0104   algo.applyConfig(cfg);
0105   algo.init();
0106 
0107   edm4hep::EventHeaderCollection headers;
0108   headers.create(0, 0); // eventNumber, runNumber
0109   edm4hep::SimTrackerHitCollection sim_hits;
0110 
0111   edm4eic::RawTrackerHitCollection raw_hits;
0112 #if EDM4EIC_BUILD_VERSION >= EDM4EIC_VERSION(8, 7, 0)
0113   edm4eic::MCRecoTrackerHitLinkCollection links;
0114 #endif
0115   edm4eic::MCRecoTrackerHitAssociationCollection associations;
0116 
0117 #if EDM4EIC_BUILD_VERSION >= EDM4EIC_VERSION(8, 7, 0)
0118   algo.process({&headers, &sim_hits}, {&raw_hits, &links, &associations});
0119 #else
0120   algo.process({&headers, &sim_hits}, {&raw_hits, &associations});
0121 #endif
0122 
0123   REQUIRE(raw_hits.size() == 0);
0124   REQUIRE(associations.size() == 0);
0125 }
0126 
0127 TEST_CASE("MPGDTrackerDigi: single hit in p-strip sensor produces raw hits", "[MPGDTrackerDigi]") {
0128   MPGDTrackerDigi algo("test_digi_single_p");
0129   auto cfg = makeDefaultConfig();
0130   algo.applyConfig(cfg);
0131   algo.init();
0132 
0133   auto id_desc     = getMPGDIdDesc();
0134   const int pStrip = 1;
0135 
0136   edm4hep::EventHeaderCollection headers;
0137   headers.create(1, 0);
0138   edm4hep::SimTrackerHitCollection sim_hits;
0139   edm4hep::MCParticleCollection mc_particles;
0140 
0141   // CellID for p-strip (strip=1), module 0, layer 0, system 3
0142   auto cellID = makeCellID(id_desc, 3, 0, 0, 0, pStrip, 0, 0);
0143 
0144   // Position: center of p-strip sensor at z = -0.025 (inside module 0 at z = 0)
0145   // Momentum along z (perpendicular to sensor) for simple traversal
0146   double eDep = 1.0e-6; // 1 keV in GeV (EDM4hep energy unit)
0147   createSimHit(sim_hits, mc_particles, cellID, 0.0, 0.0, -0.025, // position (mm)
0148                0.0, 0.0, 1.0,                                    // momentum (GeV)
0149                eDep, 10.0,                                       // eDep (GeV), time (ns)
0150                0.05); // pathLength (mm) = sensor thickness
0151 
0152   edm4eic::RawTrackerHitCollection raw_hits;
0153 #if EDM4EIC_BUILD_VERSION >= EDM4EIC_VERSION(8, 7, 0)
0154   edm4eic::MCRecoTrackerHitLinkCollection links;
0155 #endif
0156   edm4eic::MCRecoTrackerHitAssociationCollection associations;
0157 
0158 #if EDM4EIC_BUILD_VERSION >= EDM4EIC_VERSION(8, 7, 0)
0159   algo.process({&headers, &sim_hits}, {&raw_hits, &links, &associations});
0160 #else
0161   algo.process({&headers, &sim_hits}, {&raw_hits, &associations});
0162 #endif
0163 
0164   // A single sim hit should produce raw hits for both p and n strips (2-hit clusters each)
0165   REQUIRE(raw_hits.size() > 0);
0166   // Each raw hit should have a nonzero charge
0167   for (size_t i = 0; i < raw_hits.size(); i++) {
0168     CHECK(raw_hits[i].getCharge() > 0);
0169   }
0170   // Associations should link raw hits back to the sim hit
0171   CHECK(associations.size() > 0);
0172 }
0173 
0174 TEST_CASE("MPGDTrackerDigi: single hit in n-strip sensor produces raw hits", "[MPGDTrackerDigi]") {
0175   MPGDTrackerDigi algo("test_digi_single_n");
0176   auto cfg = makeDefaultConfig();
0177   algo.applyConfig(cfg);
0178   algo.init();
0179 
0180   auto id_desc     = getMPGDIdDesc();
0181   const int nStrip = 2;
0182 
0183   edm4hep::EventHeaderCollection headers;
0184   headers.create(2, 0);
0185   edm4hep::SimTrackerHitCollection sim_hits;
0186   edm4hep::MCParticleCollection mc_particles;
0187 
0188   // CellID for n-strip (strip=2), module 0
0189   auto cellID = makeCellID(id_desc, 3, 0, 0, 0, nStrip, 0, 0);
0190 
0191   // Position: center of n-strip sensor at z = +0.025 (inside module 0 at z = 0)
0192   createSimHit(sim_hits, mc_particles, cellID, 0.0, 0.0, 0.025, 0.0, 0.0, 1.0, 1.0e-6, 10.0, 0.05);
0193 
0194   edm4eic::RawTrackerHitCollection raw_hits;
0195 #if EDM4EIC_BUILD_VERSION >= EDM4EIC_VERSION(8, 7, 0)
0196   edm4eic::MCRecoTrackerHitLinkCollection links;
0197 #endif
0198   edm4eic::MCRecoTrackerHitAssociationCollection associations;
0199 
0200 #if EDM4EIC_BUILD_VERSION >= EDM4EIC_VERSION(8, 7, 0)
0201   algo.process({&headers, &sim_hits}, {&raw_hits, &links, &associations});
0202 #else
0203   algo.process({&headers, &sim_hits}, {&raw_hits, &associations});
0204 #endif
0205 
0206   REQUIRE(raw_hits.size() > 0);
0207   for (size_t i = 0; i < raw_hits.size(); i++) {
0208     CHECK(raw_hits[i].getCharge() > 0);
0209   }
0210   CHECK(associations.size() > 0);
0211 }
0212 
0213 TEST_CASE("MPGDTrackerDigi: hit below threshold produces no output", "[MPGDTrackerDigi]") {
0214   MPGDTrackerDigi algo("test_digi_threshold");
0215   auto cfg      = makeDefaultConfig();
0216   cfg.threshold = 1.0e-3; // 1 MeV threshold (high, in DD4hep units = GeV)
0217   algo.applyConfig(cfg);
0218   algo.init();
0219 
0220   auto id_desc     = getMPGDIdDesc();
0221   const int pStrip = 1;
0222 
0223   edm4hep::EventHeaderCollection headers;
0224   headers.create(3, 0);
0225   edm4hep::SimTrackerHitCollection sim_hits;
0226   edm4hep::MCParticleCollection mc_particles;
0227 
0228   auto cellID = makeCellID(id_desc, 3, 0, 0, 0, pStrip, 0, 0);
0229 
0230   // Very low energy deposit: 0.1 keV = 1e-7 GeV, well below 1 MeV threshold
0231   createSimHit(sim_hits, mc_particles, cellID, 0.0, 0.0, -0.025, 0.0, 0.0, 1.0, 1.0e-7, 10.0, 0.05);
0232 
0233   edm4eic::RawTrackerHitCollection raw_hits;
0234 #if EDM4EIC_BUILD_VERSION >= EDM4EIC_VERSION(8, 7, 0)
0235   edm4eic::MCRecoTrackerHitLinkCollection links;
0236 #endif
0237   edm4eic::MCRecoTrackerHitAssociationCollection associations;
0238 
0239 #if EDM4EIC_BUILD_VERSION >= EDM4EIC_VERSION(8, 7, 0)
0240   algo.process({&headers, &sim_hits}, {&raw_hits, &links, &associations});
0241 #else
0242   algo.process({&headers, &sim_hits}, {&raw_hits, &associations});
0243 #endif
0244 
0245   // Below threshold → no raw hits produced
0246   REQUIRE(raw_hits.size() == 0);
0247 }
0248 
0249 TEST_CASE("MPGDTrackerDigi: charge scales with energy deposit", "[MPGDTrackerDigi]") {
0250   auto id_desc     = getMPGDIdDesc();
0251   const int pStrip = 1;
0252 
0253   // Run with low energy deposit
0254   int totalChargeLow = 0;
0255   {
0256     MPGDTrackerDigi algo("test_digi_charge_low");
0257     auto cfg = makeDefaultConfig();
0258     algo.applyConfig(cfg);
0259     algo.init();
0260 
0261     edm4hep::EventHeaderCollection headers;
0262     headers.create(4, 0);
0263     edm4hep::SimTrackerHitCollection sim_hits;
0264     edm4hep::MCParticleCollection mc_particles;
0265 
0266     auto cellID = makeCellID(id_desc, 3, 0, 0, 0, pStrip, 0, 0);
0267     createSimHit(sim_hits, mc_particles, cellID, 0.0, 0.0, -0.025, 0.0, 0.0, 1.0, 1.0e-6, 10.0,
0268                  0.05); // 1 keV
0269 
0270     edm4eic::RawTrackerHitCollection raw_hits;
0271 #if EDM4EIC_BUILD_VERSION >= EDM4EIC_VERSION(8, 7, 0)
0272     edm4eic::MCRecoTrackerHitLinkCollection links;
0273 #endif
0274     edm4eic::MCRecoTrackerHitAssociationCollection associations;
0275 
0276 #if EDM4EIC_BUILD_VERSION >= EDM4EIC_VERSION(8, 7, 0)
0277     algo.process({&headers, &sim_hits}, {&raw_hits, &links, &associations});
0278 #else
0279     algo.process({&headers, &sim_hits}, {&raw_hits, &associations});
0280 #endif
0281 
0282     for (size_t i = 0; i < raw_hits.size(); i++) {
0283       totalChargeLow += raw_hits[i].getCharge();
0284     }
0285   }
0286 
0287   // Run with high energy deposit (10x)
0288   int totalChargeHigh = 0;
0289   {
0290     MPGDTrackerDigi algo("test_digi_charge_high");
0291     auto cfg = makeDefaultConfig();
0292     algo.applyConfig(cfg);
0293     algo.init();
0294 
0295     edm4hep::EventHeaderCollection headers;
0296     headers.create(5, 0);
0297     edm4hep::SimTrackerHitCollection sim_hits;
0298     edm4hep::MCParticleCollection mc_particles;
0299 
0300     auto cellID = makeCellID(id_desc, 3, 0, 0, 0, pStrip, 0, 0);
0301     createSimHit(sim_hits, mc_particles, cellID, 0.0, 0.0, -0.025, 0.0, 0.0, 1.0, 10.0e-6, 10.0,
0302                  0.05); // 10 keV
0303 
0304     edm4eic::RawTrackerHitCollection raw_hits;
0305 #if EDM4EIC_BUILD_VERSION >= EDM4EIC_VERSION(8, 7, 0)
0306     edm4eic::MCRecoTrackerHitLinkCollection links;
0307 #endif
0308     edm4eic::MCRecoTrackerHitAssociationCollection associations;
0309 
0310 #if EDM4EIC_BUILD_VERSION >= EDM4EIC_VERSION(8, 7, 0)
0311     algo.process({&headers, &sim_hits}, {&raw_hits, &links, &associations});
0312 #else
0313     algo.process({&headers, &sim_hits}, {&raw_hits, &associations});
0314 #endif
0315 
0316     for (size_t i = 0; i < raw_hits.size(); i++) {
0317       totalChargeHigh += raw_hits[i].getCharge();
0318     }
0319   }
0320 
0321   // Higher energy deposit should produce proportionally higher total charge
0322   REQUIRE(totalChargeLow > 0);
0323   REQUIRE(totalChargeHigh > 0);
0324   CHECK(totalChargeHigh > totalChargeLow);
0325 }
0326 
0327 TEST_CASE("MPGDTrackerDigi: different modules produce independent raw hits", "[MPGDTrackerDigi]") {
0328   MPGDTrackerDigi algo("test_digi_modules");
0329   auto cfg = makeDefaultConfig();
0330   algo.applyConfig(cfg);
0331   algo.init();
0332 
0333   auto id_desc     = getMPGDIdDesc();
0334   const int pStrip = 1;
0335 
0336   edm4hep::EventHeaderCollection headers;
0337   headers.create(6, 0);
0338   edm4hep::SimTrackerHitCollection sim_hits;
0339   edm4hep::MCParticleCollection mc_particles;
0340 
0341   // Hit in module 0 (z = 0, sensor p-strip at z = -0.025)
0342   auto cellID0 = makeCellID(id_desc, 3, 0, 0, 0, pStrip, 0, 0);
0343   createSimHit(sim_hits, mc_particles, cellID0, 0.0, 0.0, -0.025, 0.0, 0.0, 1.0, 1.0e-6, 10.0,
0344                0.05);
0345 
0346   // Hit in module 1 (z = 0.5, sensor p-strip at z = 0.5 - 0.025 = 0.475)
0347   auto cellID1 = makeCellID(id_desc, 3, 0, 1, 0, pStrip, 0, 0);
0348   createSimHit(sim_hits, mc_particles, cellID1, 0.0, 0.0, 0.475, 0.0, 0.0, 1.0, 1.0e-6, 20.0, 0.05);
0349 
0350   edm4eic::RawTrackerHitCollection raw_hits;
0351 #if EDM4EIC_BUILD_VERSION >= EDM4EIC_VERSION(8, 7, 0)
0352   edm4eic::MCRecoTrackerHitLinkCollection links;
0353 #endif
0354   edm4eic::MCRecoTrackerHitAssociationCollection associations;
0355 
0356 #if EDM4EIC_BUILD_VERSION >= EDM4EIC_VERSION(8, 7, 0)
0357   algo.process({&headers, &sim_hits}, {&raw_hits, &links, &associations});
0358 #else
0359   algo.process({&headers, &sim_hits}, {&raw_hits, &associations});
0360 #endif
0361 
0362   // Two independent sim hits should produce raw hits from each
0363   // (each produces hits for both p and n strips)
0364   REQUIRE(raw_hits.size() >= 4);
0365   // Each sim hit creates at least 2 associations (one per strip direction)
0366   CHECK(associations.size() >= 2);
0367 }
0368 
0369 TEST_CASE("MPGDTrackerDigi: gain parameter affects charge", "[MPGDTrackerDigi]") {
0370   auto id_desc     = getMPGDIdDesc();
0371   const int pStrip = 1;
0372 
0373   int totalChargeDefaultGain = 0;
0374   {
0375     MPGDTrackerDigi algo("test_digi_gain_default");
0376     auto cfg = makeDefaultConfig();
0377     cfg.gain = 10000;
0378     algo.applyConfig(cfg);
0379     algo.init();
0380 
0381     edm4hep::EventHeaderCollection headers;
0382     headers.create(7, 0);
0383     edm4hep::SimTrackerHitCollection sim_hits;
0384     edm4hep::MCParticleCollection mc_particles;
0385 
0386     auto cellID = makeCellID(id_desc, 3, 0, 0, 0, pStrip, 0, 0);
0387     createSimHit(sim_hits, mc_particles, cellID, 0.0, 0.0, -0.025, 0.0, 0.0, 1.0, 1.0e-6, 10.0,
0388                  0.05);
0389 
0390     edm4eic::RawTrackerHitCollection raw_hits;
0391 #if EDM4EIC_BUILD_VERSION >= EDM4EIC_VERSION(8, 7, 0)
0392     edm4eic::MCRecoTrackerHitLinkCollection links;
0393 #endif
0394     edm4eic::MCRecoTrackerHitAssociationCollection associations;
0395 
0396 #if EDM4EIC_BUILD_VERSION >= EDM4EIC_VERSION(8, 7, 0)
0397     algo.process({&headers, &sim_hits}, {&raw_hits, &links, &associations});
0398 #else
0399     algo.process({&headers, &sim_hits}, {&raw_hits, &associations});
0400 #endif
0401 
0402     for (size_t i = 0; i < raw_hits.size(); i++) {
0403       totalChargeDefaultGain += raw_hits[i].getCharge();
0404     }
0405   }
0406 
0407   int totalChargeDoubleGain = 0;
0408   {
0409     MPGDTrackerDigi algo("test_digi_gain_double");
0410     auto cfg = makeDefaultConfig();
0411     cfg.gain = 20000; // Double the gain
0412     algo.applyConfig(cfg);
0413     algo.init();
0414 
0415     edm4hep::EventHeaderCollection headers;
0416     headers.create(8, 0);
0417     edm4hep::SimTrackerHitCollection sim_hits;
0418     edm4hep::MCParticleCollection mc_particles;
0419 
0420     auto cellID = makeCellID(id_desc, 3, 0, 0, 0, pStrip, 0, 0);
0421     createSimHit(sim_hits, mc_particles, cellID, 0.0, 0.0, -0.025, 0.0, 0.0, 1.0, 1.0e-6, 10.0,
0422                  0.05);
0423 
0424     edm4eic::RawTrackerHitCollection raw_hits;
0425 #if EDM4EIC_BUILD_VERSION >= EDM4EIC_VERSION(8, 7, 0)
0426     edm4eic::MCRecoTrackerHitLinkCollection links;
0427 #endif
0428     edm4eic::MCRecoTrackerHitAssociationCollection associations;
0429 
0430 #if EDM4EIC_BUILD_VERSION >= EDM4EIC_VERSION(8, 7, 0)
0431     algo.process({&headers, &sim_hits}, {&raw_hits, &links, &associations});
0432 #else
0433     algo.process({&headers, &sim_hits}, {&raw_hits, &associations});
0434 #endif
0435 
0436     for (size_t i = 0; i < raw_hits.size(); i++) {
0437       totalChargeDoubleGain += raw_hits[i].getCharge();
0438     }
0439   }
0440 
0441   // Double gain → double total charge
0442   REQUIRE(totalChargeDefaultGain > 0);
0443   REQUIRE(totalChargeDoubleGain > 0);
0444   // Allow some tolerance for rounding (charge is integer)
0445   double ratio = static_cast<double>(totalChargeDoubleGain) / totalChargeDefaultGain;
0446   CHECK(ratio == Catch::Approx(2.0).epsilon(0.01));
0447 }
0448 
0449 TEST_CASE("MPGDTrackerDigi: raw hit timestamps reflect sim hit time", "[MPGDTrackerDigi]") {
0450   MPGDTrackerDigi algo("test_digi_timing");
0451   auto cfg           = makeDefaultConfig();
0452   cfg.timeResolution = 0.0; // No smearing for deterministic test
0453   algo.applyConfig(cfg);
0454   algo.init();
0455 
0456   auto id_desc     = getMPGDIdDesc();
0457   const int pStrip = 1;
0458 
0459   edm4hep::EventHeaderCollection headers;
0460   headers.create(9, 0);
0461   edm4hep::SimTrackerHitCollection sim_hits;
0462   edm4hep::MCParticleCollection mc_particles;
0463 
0464   double simTime = 100.0; // ns
0465   auto cellID    = makeCellID(id_desc, 3, 0, 0, 0, pStrip, 0, 0);
0466   createSimHit(sim_hits, mc_particles, cellID, 0.0, 0.0, -0.025, 0.0, 0.0, 1.0, 1.0e-6, simTime,
0467                0.05);
0468 
0469   edm4eic::RawTrackerHitCollection raw_hits;
0470 #if EDM4EIC_BUILD_VERSION >= EDM4EIC_VERSION(8, 7, 0)
0471   edm4eic::MCRecoTrackerHitLinkCollection links;
0472 #endif
0473   edm4eic::MCRecoTrackerHitAssociationCollection associations;
0474 
0475 #if EDM4EIC_BUILD_VERSION >= EDM4EIC_VERSION(8, 7, 0)
0476   algo.process({&headers, &sim_hits}, {&raw_hits, &links, &associations});
0477 #else
0478   algo.process({&headers, &sim_hits}, {&raw_hits, &associations});
0479 #endif
0480 
0481   REQUIRE(raw_hits.size() > 0);
0482   // Timestamp is stored in ps: time_ns * 1e3
0483   // With zero time resolution, timestamps should be close to simTime * 1e3
0484   double expectedTimestamp = simTime * 1e3; // ps
0485   for (size_t i = 0; i < raw_hits.size(); i++) {
0486     double ts = static_cast<double>(raw_hits[i].getTimeStamp());
0487     // Allow some tolerance for the ToF correction applied during coalesce/extend
0488     CHECK(std::abs(ts - expectedTimestamp) < 1000.0); // within 1 ns
0489   }
0490 }
0491 
0492 TEST_CASE("MPGDTrackerDigi: associations link raw hits to sim hits", "[MPGDTrackerDigi]") {
0493   MPGDTrackerDigi algo("test_digi_assoc");
0494   auto cfg = makeDefaultConfig();
0495   algo.applyConfig(cfg);
0496   algo.init();
0497 
0498   auto id_desc     = getMPGDIdDesc();
0499   const int pStrip = 1;
0500 
0501   edm4hep::EventHeaderCollection headers;
0502   headers.create(10, 0);
0503   edm4hep::SimTrackerHitCollection sim_hits;
0504   edm4hep::MCParticleCollection mc_particles;
0505 
0506   auto cellID = makeCellID(id_desc, 3, 0, 0, 0, pStrip, 0, 0);
0507   createSimHit(sim_hits, mc_particles, cellID, 0.0, 0.0, -0.025, 0.0, 0.0, 1.0, 1.0e-6, 10.0, 0.05);
0508 
0509   edm4eic::RawTrackerHitCollection raw_hits;
0510 #if EDM4EIC_BUILD_VERSION >= EDM4EIC_VERSION(8, 7, 0)
0511   edm4eic::MCRecoTrackerHitLinkCollection links;
0512 #endif
0513   edm4eic::MCRecoTrackerHitAssociationCollection associations;
0514 
0515 #if EDM4EIC_BUILD_VERSION >= EDM4EIC_VERSION(8, 7, 0)
0516   algo.process({&headers, &sim_hits}, {&raw_hits, &links, &associations});
0517 #else
0518   algo.process({&headers, &sim_hits}, {&raw_hits, &associations});
0519 #endif
0520 
0521   REQUIRE(raw_hits.size() > 0);
0522   REQUIRE(associations.size() > 0);
0523 
0524   // Each association should have weight 1.0 and reference valid sim hit
0525   for (size_t i = 0; i < associations.size(); i++) {
0526     CHECK(associations[i].getWeight() == Catch::Approx(1.0));
0527     // The associated sim hit should match our input
0528     CHECK(associations[i].getSimHit().getCellID() == cellID);
0529   }
0530 
0531 #if EDM4EIC_BUILD_VERSION >= EDM4EIC_VERSION(8, 7, 0)
0532   // Links should also be created
0533   REQUIRE(links.size() > 0);
0534   for (size_t i = 0; i < links.size(); i++) {
0535     CHECK(links[i].getWeight() == Catch::Approx(1.0));
0536   }
0537 #endif
0538 }
0539 
0540 TEST_CASE("MPGDTrackerDigi: produces both p-strip and n-strip raw hits", "[MPGDTrackerDigi]") {
0541   MPGDTrackerDigi algo("test_digi_pn_strips");
0542   auto cfg = makeDefaultConfig();
0543   algo.applyConfig(cfg);
0544   algo.init();
0545 
0546   auto id_desc     = getMPGDIdDesc();
0547   const int pStrip = 1;
0548 
0549   edm4hep::EventHeaderCollection headers;
0550   headers.create(11, 0);
0551   edm4hep::SimTrackerHitCollection sim_hits;
0552   edm4hep::MCParticleCollection mc_particles;
0553 
0554   // A hit in the p-strip sensor; the algorithm processes both p and n strips
0555   auto cellID = makeCellID(id_desc, 3, 0, 0, 0, pStrip, 0, 0);
0556   createSimHit(sim_hits, mc_particles, cellID, 0.0, 0.0, -0.025, 0.0, 0.0, 1.0, 5.0e-6, 10.0, 0.05);
0557 
0558   edm4eic::RawTrackerHitCollection raw_hits;
0559 #if EDM4EIC_BUILD_VERSION >= EDM4EIC_VERSION(8, 7, 0)
0560   edm4eic::MCRecoTrackerHitLinkCollection links;
0561 #endif
0562   edm4eic::MCRecoTrackerHitAssociationCollection associations;
0563 
0564 #if EDM4EIC_BUILD_VERSION >= EDM4EIC_VERSION(8, 7, 0)
0565   algo.process({&headers, &sim_hits}, {&raw_hits, &links, &associations});
0566 #else
0567   algo.process({&headers, &sim_hits}, {&raw_hits, &associations});
0568 #endif
0569 
0570   // The algorithm produces 2-hit clusters for each of p and n strip directions
0571   // So we expect at least 2 raw hits (one or two per strip direction)
0572   REQUIRE(raw_hits.size() >= 2);
0573 
0574   // Check that raw hits span both strip types by examining cellIDs
0575   auto decoder   = id_desc.decoder();
0576   bool hasPStrip = false, hasNStrip = false;
0577   for (size_t i = 0; i < raw_hits.size(); i++) {
0578     auto cid     = raw_hits[i].getCellID();
0579     int stripVal = decoder->get(cid, "strip");
0580     if (stripVal == 1)
0581       hasPStrip = true;
0582     if (stripVal == 2)
0583       hasNStrip = true;
0584   }
0585   CHECK(hasPStrip);
0586   CHECK(hasNStrip);
0587 }