Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-08-18 08:30:23

0001 // SPDX-License-Identifier: LGPL-3.0-or-later
0002 // Copyright (C) 2025 Minjung Kim, Joshua Sobaljic, Shujie Li
0003 //
0004 // RandomNoisePixel caches the discrete addressable pixels of each sensitive
0005 // component in compact ranges, then samples noise from one global per-pixel rate.
0006 //
0007 // If changing this, please also make necessary updates to the
0008 // docs/design/svt_pixel_noise_injection.md
0009 
0010 #include "RandomNoisePixel.h"
0011 
0012 #include <DD4hep/Alignments.h>
0013 #include <DD4hep/IDDescriptor.h>
0014 #include <DD4hep/Objects.h>
0015 #include <DD4hep/Segmentations.h>
0016 #include <DD4hep/Volumes.h>
0017 #include <DDSegmentation/BitFieldCoder.h>
0018 #include <DDSegmentation/CartesianGridXY.h>
0019 #include <DDSegmentation/CartesianGridXZ.h>
0020 #include <DDSegmentation/CylindricalGridPhiZ.h>
0021 #include <DDSegmentation/MultiSegmentation.h>
0022 #include <DDSegmentation/Segmentation.h>
0023 #include <Math/GenVector/Cartesian3D.h>
0024 #include <Math/GenVector/DisplacementVector3D.h>
0025 #include <RtypesCore.h>
0026 #include <TGeoBBox.h>
0027 #include <TGeoNode.h>
0028 #include <TGeoShape.h>
0029 #include <TGeoTube.h>
0030 #include <algorithms/geo.h>
0031 #include <algorithm>
0032 #include <array>
0033 #include <cctype>
0034 #include <cmath>
0035 #include <gsl/pointers>
0036 #include <initializer_list>
0037 #include <iterator>
0038 #include <limits>
0039 #include <mutex>
0040 #include <numbers>
0041 #include <optional>
0042 #include <stdexcept>
0043 #include <tuple>
0044 #include <utility>
0045 
0046 #include "algorithms/digi/RandomNoisePixelConfig.h"
0047 
0048 namespace eicrecon {
0049 namespace {
0050 
0051   using RawSegmentation = dd4hep::DDSegmentation::Segmentation;
0052 
0053   // ROOT's geometry navigator is shared. This mutex protects only the short
0054   // initialization phase in which global positions are converted to cell IDs.
0055   std::mutex& geometryInitializationMutex() {
0056     static std::mutex mutex;
0057     return mutex;
0058   }
0059 
0060   // Make DD4hep volume-ID field matching insensitive to capitalization.
0061   std::string lower(std::string value) {
0062     std::transform(value.begin(), value.end(), value.begin(),
0063                    [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
0064     return value;
0065   }
0066 
0067   // Read the layer number from a placed volume's explicit DD4hep volume IDs.
0068   std::optional<int> layerFromPlacement(const dd4hep::PlacedVolume& placement) {
0069     if (!placement.isValid()) {
0070       return std::nullopt;
0071     }
0072     for (const auto& [field, value] : placement.volIDs()) {
0073       const auto name = lower(field);
0074       if (name == "layer" || name.find("lay") != std::string::npos) {
0075         return value;
0076       }
0077     }
0078     return std::nullopt;
0079   }
0080 
0081   // Prefer the layer placement ID, then try the module placement for geometries
0082   // that attach the layer field one level lower in the hierarchy.
0083   std::optional<int> layerFromDetElement(const dd4hep::DetElement& layer,
0084                                          const dd4hep::DetElement& module) {
0085     if (auto id = layerFromPlacement(layer.placement())) {
0086       return id;
0087     }
0088     return layerFromPlacement(module.placement());
0089   }
0090 
0091   // Copy a placed volume's local-to-parent transformation into an owned matrix.
0092   TGeoHMatrix placementTransform(const dd4hep::PlacedVolume& placement) {
0093     if (auto* node = placement.ptr()) {
0094       if (auto* matrix = node->GetMatrix()) {
0095         return TGeoHMatrix{*matrix};
0096       }
0097     }
0098     return TGeoHMatrix{};
0099   }
0100 
0101   // Compose child-local -> parent-local with parent-local -> world.
0102   TGeoHMatrix composeTransforms(const TGeoHMatrix& parentToWorld,
0103                                 const TGeoHMatrix& childToParent) {
0104     TGeoHMatrix childToWorld{parentToWorld};
0105     childToWorld.Multiply(childToParent);
0106     return childToWorld;
0107   }
0108 
0109   // Accept a sensitive volume only when its readout exactly matches this
0110   // RandomNoisePixel instance (for example, SiBarrelHits but not MPGD hits).
0111   bool hasRequestedReadout(const dd4hep::PlacedVolume& placement, std::string_view readoutName) {
0112     if (!placement.isValid()) {
0113       return false;
0114     }
0115     dd4hep::SensitiveDetector sensitive{placement.volume().sensitiveDetector()};
0116     if (!sensitive.isValid()) {
0117       return false;
0118     }
0119     const auto readout = sensitive.readout();
0120     return readout.isValid() && readout.name() == readoutName;
0121   }
0122 
0123   // Record the shape and transform needed to identify one sensitive placement.
0124   // This initialization-only information is discarded after the cache is built.
0125   RandomNoisePixel::SensitiveComponentGeometry
0126   makeComponentGeometry(const std::string& detectorName, int layer,
0127                         const dd4hep::PlacedVolume& placement, const TGeoHMatrix& localToWorld) {
0128     auto volume = placement.volume();
0129     if (!volume.isValid() || !volume.ptr() || !volume->GetShape()) {
0130       throw std::runtime_error("RandomNoisePixel sensitive volume has no TGeo shape");
0131     }
0132     return {.detectorName          = detectorName,
0133             .layer                 = layer,
0134             .volume                = volume.ptr(),
0135             .localToWorldTransform = localToWorld};
0136   }
0137 
0138   // Recursively search a module assembly for sensitive descendants while
0139   // accumulating the complete sensitive-local to world transformation.
0140   void
0141   collectSensitiveDescendants(const std::string& detectorName, int layer,
0142                               const dd4hep::PlacedVolume& parent, const TGeoHMatrix& parentToWorld,
0143                               std::string_view readoutName,
0144                               std::vector<RandomNoisePixel::SensitiveComponentGeometry>& output) {
0145     auto* node = parent.ptr();
0146     if (!node) {
0147       return;
0148     }
0149 
0150     for (int i = 0; i < node->GetNdaughters(); ++i) {
0151       auto* daughterNode = node->GetDaughter(i);
0152       if (!daughterNode) {
0153         continue;
0154       }
0155       dd4hep::PlacedVolume daughter{daughterNode};
0156       const auto daughterToWorld = composeTransforms(parentToWorld, placementTransform(daughter));
0157       if (hasRequestedReadout(daughter, readoutName)) {
0158         output.push_back(makeComponentGeometry(detectorName, layer, daughter, daughterToWorld));
0159       } else {
0160         collectSensitiveDescendants(detectorName, layer, daughter, daughterToWorld, readoutName,
0161                                     output);
0162       }
0163     }
0164   }
0165 
0166   // Return all matching sensitive pieces belonging to one module. Some modules
0167   // are directly sensitive; others contain two or more sensitive daughters.
0168   std::vector<RandomNoisePixel::SensitiveComponentGeometry>
0169   findSensitiveComponents(const std::string& detectorName, int layer,
0170                           const dd4hep::DetElement& module, std::string_view readoutName) {
0171     const auto modulePlacement = module.placement();
0172     if (!modulePlacement.isValid()) {
0173       return {};
0174     }
0175 
0176     std::vector<RandomNoisePixel::SensitiveComponentGeometry> components;
0177     const auto moduleToWorld = module.nominal().worldTransformation();
0178     if (hasRequestedReadout(modulePlacement, readoutName)) {
0179       components.push_back(
0180           makeComponentGeometry(detectorName, layer, modulePlacement, moduleToWorld));
0181     } else {
0182       collectSensitiveDescendants(detectorName, layer, modulePlacement, moduleToWorld, readoutName,
0183                                   components);
0184     }
0185     return components;
0186   }
0187 
0188   // Access the axis-aligned local bounds supplied by ROOT for the sensor shape.
0189   const TGeoBBox& boundingBox(const RandomNoisePixel::SensitiveComponentGeometry& component) {
0190     const auto* box = dynamic_cast<const TGeoBBox*>(component.volume->GetShape());
0191     if (!box) {
0192       throw std::runtime_error("RandomNoisePixel requires a TGeo shape with bounding-box data");
0193     }
0194     return *box;
0195   }
0196 
0197   // Choose a point safely inside the sensor. Tube segments require their radial
0198   // and angular midpoint because the center of their bounding box can be empty.
0199   std::array<double, 3>
0200   referenceLocalPoint(const RandomNoisePixel::SensitiveComponentGeometry& component) {
0201     if (const auto* tube = dynamic_cast<const TGeoTubeSeg*>(component.volume->GetShape())) {
0202       const double radius = 0.5 * (tube->GetRmin() + tube->GetRmax());
0203       const double phi    = 0.5 * (tube->GetPhi1() + tube->GetPhi2()) * std::numbers::pi / 180.0;
0204       return {radius * std::cos(phi), radius * std::sin(phi), 0.0};
0205     }
0206 
0207     const auto& box    = boundingBox(component);
0208     const auto* origin = box.GetOrigin();
0209     return {origin[0], origin[1], origin[2]};
0210   }
0211 
0212   // Resolve a MultiSegmentation to the concrete grid selected by fields already
0213   // present in the sensor's base volume ID (the BVTX discriminator is layer).
0214   const RawSegmentation& selectedSegmentation(const dd4hep::Segmentation& segmentation,
0215                                               std::uint64_t baseVolumeID) {
0216     const RawSegmentation* selected = segmentation.segmentation();
0217     while (selected && selected->type() == "MultiSegmentation") {
0218       const auto* multi = dynamic_cast<const dd4hep::DDSegmentation::MultiSegmentation*>(selected);
0219       if (!multi) {
0220         throw std::runtime_error("RandomNoisePixel cannot access MultiSegmentation");
0221       }
0222       selected = &multi->subsegmentation(baseVolumeID);
0223     }
0224     if (!selected) {
0225       throw std::runtime_error("RandomNoisePixel readout has no segmentation");
0226     }
0227     return *selected;
0228   }
0229 
0230   // Convert a physical coordinate interval into the inclusive integer indices
0231   // of pixel centers inside it, then clamp to the cell-ID field's legal range.
0232   std::pair<std::int64_t, std::int64_t>
0233   indexRange(double minimum, double maximum, double pitch, double offset,
0234              const dd4hep::DDSegmentation::BitFieldElement& field) {
0235     if (!(pitch > 0.0) || !std::isfinite(pitch)) {
0236       throw std::runtime_error("RandomNoisePixel found an invalid segmentation pitch");
0237     }
0238     constexpr double indexTolerance = 1.0e-9;
0239     auto first = static_cast<std::int64_t>(std::ceil((minimum - offset) / pitch - indexTolerance));
0240     auto last  = static_cast<std::int64_t>(std::floor((maximum - offset) / pitch + indexTolerance));
0241     first      = std::max(first, static_cast<std::int64_t>(field.minValue()));
0242     last       = std::min(last, static_cast<std::int64_t>(field.maxValue()));
0243     return {first, last};
0244   }
0245 
0246   // A true box can be represented by one rectangular range without row tests.
0247   bool isPureBox(const TGeoShape& shape) {
0248     return std::string_view{shape.ClassName()} == "TGeoBBox";
0249   }
0250 
0251   // ROOT may classify a mathematically boundary-centered pixel as just outside
0252   // after a local -> global -> local floating-point round trip. Accept only
0253   // points within ROOT's own surface tolerance; larger misses remain errors.
0254   bool containsOrTouches(const TGeoShape& shape, const double point[3]) {
0255     return shape.Contains(point) || shape.Safety(point, false) <= 10.0 * TGeoShape::Tolerance();
0256   }
0257 
0258   // Build the exact discrete pixel layout for Cartesian XY or XZ grids. Boxes
0259   // become rectangles; trapezoids and clipped sensors become compact row spans.
0260   std::shared_ptr<RandomNoisePixel::PixelLayout>
0261   makeCartesianLayout(const RandomNoisePixel::SensitiveComponentGeometry& component,
0262                       RandomNoisePixel::GridKind kind, std::string firstField,
0263                       std::string secondField, double firstPitch, double secondPitch,
0264                       double firstOffset, double secondOffset,
0265                       const dd4hep::DDSegmentation::BitFieldCoder& decoder) {
0266     const auto& box           = boundingBox(component);
0267     const auto* origin        = box.GetOrigin();
0268     const bool xy             = kind == RandomNoisePixel::GridKind::CartesianXY;
0269     const double firstCenter  = origin[0];
0270     const double firstHalf    = box.GetDX();
0271     const double secondCenter = xy ? origin[1] : origin[2];
0272     const double secondHalf   = xy ? box.GetDY() : box.GetDZ();
0273     // Step 1: turn the sensor's physical bounds into candidate pixel-index bounds.
0274     const auto [firstMin, firstMax] = indexRange(firstCenter - firstHalf, firstCenter + firstHalf,
0275                                                  firstPitch, firstOffset, decoder[firstField]);
0276     const auto [secondMin, secondMax] =
0277         indexRange(secondCenter - secondHalf, secondCenter + secondHalf, secondPitch, secondOffset,
0278                    decoder[secondField]);
0279 
0280     if (firstMax < firstMin || secondMax < secondMin) {
0281       throw std::runtime_error("RandomNoisePixel sensitive component contains no pixel centers");
0282     }
0283 
0284     auto layout         = std::make_shared<RandomNoisePixel::PixelLayout>();
0285     layout->kind        = kind;
0286     layout->firstField  = std::move(firstField);
0287     layout->secondField = std::move(secondField);
0288 
0289     // Step 2a: a box contains every candidate pixel center, so store four limits.
0290     if (isPureBox(*component.volume->GetShape())) {
0291       layout->rectangular    = true;
0292       layout->firstMin       = firstMin;
0293       layout->firstMax       = firstMax;
0294       layout->secondMin      = secondMin;
0295       layout->secondMax      = secondMax;
0296       const auto firstCount  = static_cast<std::uint64_t>(firstMax - firstMin + 1);
0297       const auto secondCount = static_cast<std::uint64_t>(secondMax - secondMin + 1);
0298       if (firstCount > std::numeric_limits<std::uint64_t>::max() / secondCount) {
0299         throw std::overflow_error("RandomNoisePixel pixel count overflow");
0300       }
0301       layout->totalPixels = firstCount * secondCount;
0302       return layout;
0303     }
0304 
0305     // Step 2b: for a non-box shape, test pixel centers against TGeo::Contains().
0306     const auto* shape = component.volume->GetShape();
0307     auto contains     = [&](std::int64_t firstIndex, std::int64_t secondIndex) {
0308       const double first  = firstOffset + firstPitch * static_cast<double>(firstIndex);
0309       const double second = secondOffset + secondPitch * static_cast<double>(secondIndex);
0310       double local[3]     = {origin[0], origin[1], origin[2]};
0311       local[0]            = first;
0312       if (xy) {
0313         local[1] = second;
0314       } else {
0315         local[2] = second;
0316       }
0317       return containsOrTouches(*shape, local);
0318     };
0319 
0320     // Step 3: each supported non-box sensor is convex. Find one interior pixel
0321     // near the row center, then binary-search its left and right boundaries.
0322     const auto middleGuess = std::clamp(
0323         static_cast<std::int64_t>(std::llround((firstCenter - firstOffset) / firstPitch)), firstMin,
0324         firstMax);
0325     std::uint64_t cumulative = 0;
0326     for (std::int64_t second = secondMin; second <= secondMax; ++second) {
0327       if (!contains(middleGuess, second)) {
0328         continue;
0329       }
0330 
0331       std::int64_t low  = firstMin;
0332       std::int64_t high = middleGuess;
0333       while (low < high) {
0334         const auto middle = low + (high - low) / 2;
0335         if (contains(middle, second)) {
0336           high = middle;
0337         } else {
0338           low = middle + 1;
0339         }
0340       }
0341       const auto rowFirst = low;
0342 
0343       low  = middleGuess;
0344       high = firstMax;
0345       while (low < high) {
0346         const auto middle = low + (high - low + 1) / 2;
0347         if (contains(middle, second)) {
0348           low = middle;
0349         } else {
0350           high = middle - 1;
0351         }
0352       }
0353       const auto rowLast  = low;
0354       const auto rowCount = static_cast<std::uint64_t>(rowLast - rowFirst + 1);
0355       if (cumulative > std::numeric_limits<std::uint64_t>::max() - rowCount) {
0356         throw std::overflow_error("RandomNoisePixel pixel row count overflow");
0357       }
0358       // The cumulative count maps a flat random integer back to this row.
0359       cumulative += rowCount;
0360       layout->rows.push_back({second, rowFirst, rowLast, cumulative});
0361     }
0362 
0363     if (layout->rows.empty()) {
0364       throw std::runtime_error("RandomNoisePixel found no segmentation centers inside TGeo shape");
0365     }
0366     layout->totalPixels = cumulative;
0367     return layout;
0368   }
0369 
0370   // Build a phi-z rectangle in the sensitive volume's local coordinate system.
0371   // DD4hep later applies the placement transform when converting a cell ID to
0372   // a global position, so using world bounds here would apply the placement twice.
0373   std::shared_ptr<RandomNoisePixel::PixelLayout>
0374   makeCylindricalLayout(const RandomNoisePixel::SensitiveComponentGeometry& component,
0375                         const dd4hep::DDSegmentation::CylindricalGridPhiZ& grid,
0376                         const dd4hep::DDSegmentation::BitFieldCoder& decoder) {
0377     // Step 1: establish a local central phi so angles remain continuous across +/-pi.
0378     const auto center      = referenceLocalPoint(component);
0379     const double centerPhi = std::atan2(center[1], center[0]);
0380 
0381     double phiMin          = std::numeric_limits<double>::max();
0382     double phiMax          = std::numeric_limits<double>::lowest();
0383     double zMin            = std::numeric_limits<double>::max();
0384     double zMax            = std::numeric_limits<double>::lowest();
0385     auto includeLocalPoint = [&](double x, double y, double z) {
0386       const double phi =
0387           centerPhi + std::remainder(std::atan2(y, x) - centerPhi, 2.0 * std::numbers::pi);
0388       phiMin = std::min(phiMin, phi);
0389       phiMax = std::max(phiMax, phi);
0390       zMin   = std::min(zMin, z);
0391       zMax   = std::max(zMax, z);
0392     };
0393 
0394     // Step 2: inspect the true local TubeSeg boundaries. For other supported
0395     // shapes, use local bounding-box corners; initialization validation below
0396     // rejects a layout if the segmentation and solid frames are incompatible.
0397     if (const auto* tube = dynamic_cast<const TGeoTubeSeg*>(component.volume->GetShape())) {
0398       for (double radius : {tube->GetRmin(), tube->GetRmax()}) {
0399         for (double phiDegrees : {tube->GetPhi1(), tube->GetPhi2()}) {
0400           const double phi = phiDegrees * std::numbers::pi / 180.0;
0401           for (double z : {-tube->GetDz(), tube->GetDz()}) {
0402             includeLocalPoint(radius * std::cos(phi), radius * std::sin(phi), z);
0403           }
0404         }
0405       }
0406     } else {
0407       const auto& box    = boundingBox(component);
0408       const auto* origin = box.GetOrigin();
0409       for (double sx : {-1.0, 1.0}) {
0410         for (double sy : {-1.0, 1.0}) {
0411           for (double sz : {-1.0, 1.0}) {
0412             includeLocalPoint(origin[0] + sx * box.GetDX(), origin[1] + sy * box.GetDY(),
0413                               origin[2] + sz * box.GetDZ());
0414           }
0415         }
0416       }
0417     }
0418 
0419     // Step 3: convert angular and longitudinal bounds to discrete pixel indices.
0420     const auto [phiIndexMin, phiIndexMax] = indexRange(
0421         phiMin, phiMax, grid.gridSizePhi(), grid.offsetPhi(), decoder[grid.fieldNamePhi()]);
0422     const auto [zIndexMin, zIndexMax] =
0423         indexRange(zMin, zMax, grid.gridSizeZ(), grid.offsetZ(), decoder[grid.fieldNameZ()]);
0424     if (phiIndexMax < phiIndexMin || zIndexMax < zIndexMin) {
0425       throw std::runtime_error("RandomNoisePixel cylindrical component contains no pixels");
0426     }
0427 
0428     auto layout         = std::make_shared<RandomNoisePixel::PixelLayout>();
0429     layout->kind        = RandomNoisePixel::GridKind::CylindricalPhiZ;
0430     layout->firstField  = grid.fieldNamePhi();
0431     layout->secondField = grid.fieldNameZ();
0432     layout->rectangular = true;
0433     layout->firstMin    = phiIndexMin;
0434     layout->firstMax    = phiIndexMax;
0435     layout->secondMin   = zIndexMin;
0436     layout->secondMax   = zIndexMax;
0437     const auto phiCount = static_cast<std::uint64_t>(phiIndexMax - phiIndexMin + 1);
0438     const auto zCount   = static_cast<std::uint64_t>(zIndexMax - zIndexMin + 1);
0439     if (phiCount > std::numeric_limits<std::uint64_t>::max() / zCount) {
0440       throw std::overflow_error("RandomNoisePixel cylindrical pixel count overflow");
0441     }
0442     layout->totalPixels = phiCount * zCount;
0443     return layout;
0444   }
0445 
0446   // Map one flat index in [0, totalPixels) to the two segmentation indices.
0447   // This gives every addressable pixel exactly the same selection probability.
0448   std::pair<std::int64_t, std::int64_t> pixelIndices(const RandomNoisePixel::PixelLayout& layout,
0449                                                      std::uint64_t linearIndex) {
0450     if (linearIndex >= layout.totalPixels) {
0451       throw std::out_of_range("RandomNoisePixel linear pixel index is out of range");
0452     }
0453     if (layout.rectangular) {
0454       const auto firstCount = static_cast<std::uint64_t>(layout.firstMax - layout.firstMin + 1);
0455       return {layout.firstMin + static_cast<std::int64_t>(linearIndex % firstCount),
0456               layout.secondMin + static_cast<std::int64_t>(linearIndex / firstCount)};
0457     }
0458 
0459     const auto row =
0460         std::upper_bound(layout.rows.begin(), layout.rows.end(), linearIndex,
0461                          [](std::uint64_t value, const RandomNoisePixel::PixelRow& candidate) {
0462                            return value < candidate.cumulativeEnd;
0463                          });
0464     if (row == layout.rows.end()) {
0465       throw std::out_of_range("RandomNoisePixel could not resolve compact pixel row");
0466     }
0467     const auto previousEnd = row == layout.rows.begin() ? 0 : (row - 1)->cumulativeEnd;
0468     return {row->firstMin + static_cast<std::int64_t>(linearIndex - previousEnd), row->secondIndex};
0469   }
0470 
0471 } // namespace
0472 
0473 // Build all static geometry and pixel-count metadata before event processing.
0474 void RandomNoisePixel::init() {
0475   // Step 1: reset state and validate the user-facing configuration.
0476   m_components.clear();
0477   m_layers.clear();
0478 
0479   if (!m_cfg.addNoise) {
0480     debug("RandomNoisePixel '{}': disabled by configuration; skipping geometry cache", name());
0481     return;
0482   }
0483   if (!(m_cfg.noise_rate_per_pixel_per_event >= 0.0 &&
0484         m_cfg.noise_rate_per_pixel_per_event <= 1.0)) {
0485     throw std::invalid_argument(
0486         "RandomNoisePixel noise_rate_per_pixel_per_event must be within [0, 1]");
0487   }
0488 
0489   // Step 2: CellIDPositionConverter::cellID(global) navigates shared TGeo state. JANA can
0490   // initialize detector factories concurrently, so serialize only cache construction.
0491   const std::scoped_lock geometryLock{geometryInitializationMutex()};
0492 
0493   // Step 3: obtain DD4hep services and the requested readout/segmentation.
0494   const auto& geo = algorithms::GeoSvc::instance();
0495   m_dd4hepGeo     = geo.detector();
0496   m_converter     = geo.cellIDPositionConverter();
0497   if (!m_dd4hepGeo || !m_converter) {
0498     throw std::runtime_error("RandomNoisePixel requires DD4hep geometry and cell-ID services");
0499   }
0500 
0501   m_readout = m_dd4hepGeo->readout(m_cfg.readout_name);
0502   if (!m_readout.isValid()) {
0503     throw std::invalid_argument("RandomNoisePixel invalid readout: " + m_cfg.readout_name);
0504   }
0505 
0506   // Step 4: collect geometry needed to identify each sensitive placement. This
0507   // vector owns the large TGeo transforms only during initialization.
0508   {
0509     std::vector<SensitiveComponentGeometry> componentGeometry;
0510     for (const auto& [name, detector] : m_dd4hepGeo->detectors()) {
0511       const auto sensitive = m_dd4hepGeo->sensitiveDetector(name);
0512       if (sensitive.isValid() && sensitive.readout().isValid() &&
0513           sensitive.readout().name() == m_cfg.readout_name) {
0514         collectDetectorComponents(detector, componentGeometry);
0515       }
0516     }
0517 
0518     // Step 5: convert initialization geometry into the compact event-time cache.
0519     cachePixelLayouts(componentGeometry);
0520   }
0521 
0522   // Step 6: the temporary transforms have now been released; construct layer totals.
0523   buildLayers();
0524   info("RandomNoisePixel '{}': cached {} sensitive components and {} layer groups for readout '{}'",
0525        name(), m_components.size(), m_layers.size(), m_cfg.readout_name);
0526 }
0527 
0528 // Enter each top-level layer of one detector system.
0529 void RandomNoisePixel::collectDetectorComponents(
0530     const dd4hep::DetElement& detector,
0531     std::vector<SensitiveComponentGeometry>& componentGeometry) {
0532   for (const auto& [_, layer] : detector.children()) {
0533     collectLayerComponents(detector.name(), layer, componentGeometry);
0534   }
0535 }
0536 
0537 // Collect every sensitive placement below one detector layer.
0538 void RandomNoisePixel::collectLayerComponents(
0539     const std::string& detectorName, const dd4hep::DetElement& layer,
0540     std::vector<SensitiveComponentGeometry>& componentGeometry) {
0541   for (const auto& [_, module] : layer.children()) {
0542     // Step 1: handle the common detector -> layer -> module hierarchy.
0543     const auto layerID = layerFromDetElement(layer, module);
0544     if (layerID) {
0545       auto components = findSensitiveComponents(detectorName, *layerID, module, m_cfg.readout_name);
0546       if (!components.empty()) {
0547         componentGeometry.insert(componentGeometry.end(),
0548                                  std::make_move_iterator(components.begin()),
0549                                  std::make_move_iterator(components.end()));
0550         continue;
0551       }
0552     }
0553 
0554     // Step 2: some geometries add one extra assembly level below the module.
0555     for (const auto& [__, child] : module.children()) {
0556       const auto childLayerID = layerID ? layerID : layerFromDetElement(layer, child);
0557       if (!childLayerID) {
0558         continue;
0559       }
0560       auto childComponents =
0561           findSensitiveComponents(detectorName, *childLayerID, child, m_cfg.readout_name);
0562       componentGeometry.insert(componentGeometry.end(),
0563                                std::make_move_iterator(childComponents.begin()),
0564                                std::make_move_iterator(childComponents.end()));
0565     }
0566   }
0567 }
0568 
0569 // Attach a base volume ID and a compact addressable-pixel layout to every component.
0570 void RandomNoisePixel::cachePixelLayouts(
0571     const std::vector<SensitiveComponentGeometry>& componentGeometry) {
0572   // Step 1: get the generic segmentation handle and the cell-ID bit-field encoder.
0573   const auto segmentation = m_readout.segmentation();
0574   const auto* decoder     = m_readout.idSpec().decoder();
0575   if (!segmentation.isValid() || !decoder) {
0576     throw std::runtime_error("RandomNoisePixel readout has no segmentation or ID decoder");
0577   }
0578 
0579   // Repeated Cartesian sensors share a logical TGeo volume and segmentation.
0580   // Store one immutable layout for all such placements to keep memory O(components).
0581   struct SharedLayout {
0582     const TGeoVolume* volume            = nullptr;
0583     const RawSegmentation* segmentation = nullptr;
0584     std::shared_ptr<const PixelLayout> layout;
0585   };
0586   std::vector<SharedLayout> sharedCartesianLayouts;
0587 
0588   m_components.reserve(componentGeometry.size());
0589   for (const auto& geometry : componentGeometry) {
0590     // Step 2: map one known interior point to a full DD4hep cell ID, then clear
0591     // the local pixel fields to obtain the physical sensor's base volume ID.
0592     const auto reference = referenceLocalPoint(geometry);
0593     double local[3]      = {reference[0], reference[1], reference[2]};
0594     if (!geometry.volume->GetShape()->Contains(local)) {
0595       throw std::runtime_error("RandomNoisePixel reference point is outside sensitive volume '" +
0596                                std::string{geometry.volume->GetName()} + "' with shape '" +
0597                                geometry.volume->GetShape()->ClassName() + "'");
0598     }
0599     double globalCoordinates[3];
0600     geometry.localToWorldTransform.LocalToMaster(local, globalCoordinates);
0601     const dd4hep::Position global{globalCoordinates[0], globalCoordinates[1], globalCoordinates[2]};
0602     const auto referenceCell = m_converter->cellID(global);
0603     if (!m_converter->findContext(referenceCell)) {
0604       throw std::runtime_error("RandomNoisePixel could not resolve a component reference cell ID");
0605     }
0606     const auto baseVolumeID = segmentation.volumeID(referenceCell);
0607     SensitiveComponent component;
0608     component.detectorName = geometry.detectorName;
0609     component.layer        = geometry.layer;
0610     component.baseVolumeID = baseVolumeID;
0611 
0612     // Step 3: select the concrete grid and reuse a Cartesian layout when possible.
0613     const auto& selected  = selectedSegmentation(segmentation, baseVolumeID);
0614     const auto findShared = [&]() -> std::shared_ptr<const PixelLayout> {
0615       for (const auto& cached : sharedCartesianLayouts) {
0616         if (cached.volume == geometry.volume && cached.segmentation == &selected) {
0617           return cached.layout;
0618         }
0619       }
0620       return {};
0621     };
0622 
0623     // Step 4: dispatch only to segmentation types explicitly supported by the SVT.
0624     if (const auto* grid =
0625             dynamic_cast<const dd4hep::DDSegmentation::CartesianGridXY*>(&selected)) {
0626       component.layout = findShared();
0627       if (!component.layout) {
0628         component.layout = makeCartesianLayout(
0629             geometry, GridKind::CartesianXY, grid->fieldNameX(), grid->fieldNameY(),
0630             grid->gridSizeX(), grid->gridSizeY(), grid->offsetX(), grid->offsetY(), *decoder);
0631         sharedCartesianLayouts.push_back({geometry.volume, &selected, component.layout});
0632       }
0633     } else if (const auto* grid =
0634                    dynamic_cast<const dd4hep::DDSegmentation::CartesianGridXZ*>(&selected)) {
0635       component.layout = findShared();
0636       if (!component.layout) {
0637         component.layout = makeCartesianLayout(
0638             geometry, GridKind::CartesianXZ, grid->fieldNameX(), grid->fieldNameZ(),
0639             grid->gridSizeX(), grid->gridSizeZ(), grid->offsetX(), grid->offsetZ(), *decoder);
0640         sharedCartesianLayouts.push_back({geometry.volume, &selected, component.layout});
0641       }
0642     } else if (const auto* grid =
0643                    dynamic_cast<const dd4hep::DDSegmentation::CylindricalGridPhiZ*>(&selected)) {
0644       component.layout = makeCylindricalLayout(geometry, *grid, *decoder);
0645     } else {
0646       throw std::runtime_error("RandomNoisePixel unsupported segmentation type: " +
0647                                selected.type());
0648     }
0649 
0650     // Step 5: cache the count, then choose representative addresses across the
0651     // layout. Testing the edges as well as the middle catches placement/frame
0652     // mistakes that a single central pixel could hide.
0653     component.pixelCount                             = component.layout->totalPixels;
0654     std::array<std::uint64_t, 5> sampleLinearIndices = {
0655         0, component.pixelCount / 4, component.pixelCount / 2,
0656         component.pixelCount / 2 + component.pixelCount / 4, component.pixelCount - 1};
0657     std::sort(sampleLinearIndices.begin(), sampleLinearIndices.end());
0658     const auto uniqueEnd = std::unique(sampleLinearIndices.begin(), sampleLinearIndices.end());
0659 
0660     // Step 6: round-trip each cell ID through DD4hep and verify that its center
0661     // returns inside this exact placed sensitive solid. DD4hep applies the
0662     // placement once when producing the global position; MasterToLocal removes
0663     // that same complete (possibly nested) translation and rotation here.
0664     for (auto sample = sampleLinearIndices.begin(); sample != uniqueEnd; ++sample) {
0665       const auto [sampleFirst, sampleSecond] = pixelIndices(*component.layout, *sample);
0666       auto sampleID                          = component.baseVolumeID;
0667       decoder->set(sampleID, component.layout->firstField, sampleFirst);
0668       decoder->set(sampleID, component.layout->secondField, sampleSecond);
0669       if (!m_converter->findContext(sampleID)) {
0670         throw std::runtime_error("RandomNoisePixel generated an invalid sample cell ID");
0671       }
0672 
0673       const auto sampleGlobal           = m_converter->position(sampleID);
0674       double sampleGlobalCoordinates[3] = {sampleGlobal.x(), sampleGlobal.y(), sampleGlobal.z()};
0675       double sampleLocalCoordinates[3];
0676       geometry.localToWorldTransform.MasterToLocal(sampleGlobalCoordinates, sampleLocalCoordinates);
0677       if (!containsOrTouches(*geometry.volume->GetShape(), sampleLocalCoordinates)) {
0678         throw std::runtime_error(
0679             "RandomNoisePixel sample cell center does not return inside sensitive volume '" +
0680             std::string{geometry.volume->GetName()} + "' in detector '" + component.detectorName +
0681             "' layer " + std::to_string(component.layer) + "; pixel indices=(" +
0682             std::to_string(sampleFirst) + ", " + std::to_string(sampleSecond) +
0683             "), returned local position=(" + std::to_string(sampleLocalCoordinates[0]) + ", " +
0684             std::to_string(sampleLocalCoordinates[1]) + ", " +
0685             std::to_string(sampleLocalCoordinates[2]) +
0686             "); check segmentation coordinates and placement transforms");
0687       }
0688     }
0689     m_components.push_back(std::move(component));
0690   }
0691 }
0692 
0693 // Group components by detector and layer and prepare weighted random selection.
0694 void RandomNoisePixel::buildLayers() {
0695   for (std::size_t componentIndex = 0; componentIndex < m_components.size(); ++componentIndex) {
0696     const auto& component = m_components[componentIndex];
0697     auto layer =
0698         std::find_if(m_layers.begin(), m_layers.end(), [&](const LayerGeometry& candidate) {
0699           return candidate.detectorName == component.detectorName &&
0700                  candidate.layer == component.layer;
0701         });
0702     if (layer == m_layers.end()) {
0703       m_layers.push_back({component.detectorName, component.layer, {}, {}, 0});
0704       layer = std::prev(m_layers.end());
0705     }
0706     if (layer->totalPixels > std::numeric_limits<std::uint64_t>::max() - component.pixelCount) {
0707       throw std::overflow_error("RandomNoisePixel layer pixel count overflow");
0708     }
0709     // Appending cumulative totals lets upper_bound select a component with
0710     // probability N_component / N_layer without visiting all components/event.
0711     layer->totalPixels += component.pixelCount;
0712     layer->componentIndices.push_back(componentIndex);
0713     layer->cumulativePixels.push_back(layer->totalPixels);
0714   }
0715 
0716   for (const auto& layer : m_layers) {
0717     info("RandomNoisePixel geometry: detector='{}' layer={} components={} pixels={} "
0718          "expected_hits={}",
0719          layer.detectorName, layer.layer, layer.componentIndices.size(), layer.totalPixels,
0720          m_cfg.noise_rate_per_pixel_per_event * static_cast<double>(layer.totalPixels));
0721   }
0722 }
0723 
0724 // Derive the event RNG seed from the run/event identity and algorithm name.
0725 std::uint64_t
0726 RandomNoisePixel::seedFromEventHeader(const edm4hep::EventHeaderCollection& headers) const {
0727   if (headers.empty()) {
0728     throw std::runtime_error("RandomNoisePixel requires a non-empty EventHeader collection");
0729   }
0730   return m_uid.getUniqueID(headers, name());
0731 }
0732 
0733 // Select one pixel uniformly within a sensitive component and encode its fields.
0734 std::uint64_t RandomNoisePixel::randomCellID(const SensitiveComponent& component,
0735                                              std::mt19937_64& rng) const {
0736   std::uniform_int_distribution<std::uint64_t> pickPixel(0, component.pixelCount - 1);
0737   const auto [firstIndex, secondIndex] = pixelIndices(*component.layout, pickPixel(rng));
0738   auto cellID                          = component.baseVolumeID;
0739   const auto* decoder                  = m_readout.idSpec().decoder();
0740   decoder->set(cellID, component.layout->firstField, firstIndex);
0741   decoder->set(cellID, component.layout->secondField, secondIndex);
0742   return cellID;
0743 }
0744 
0745 // Generate noise for one layer using lambda = rate_per_pixel_per_event * N_pixels.
0746 void RandomNoisePixel::addNoiseHitsForLayer(
0747     const LayerGeometry& layer, std::map<std::uint64_t, edm4eic::MutableRawTrackerHit>& hitMap,
0748     std::mt19937_64& rng) const {
0749   if (layer.totalPixels == 0 || m_cfg.noise_rate_per_pixel_per_event == 0.0) {
0750     return;
0751   }
0752 
0753   // Step 1: draw the total layer occupancy from the sparse-noise Poisson model.
0754   const double mean = m_cfg.noise_rate_per_pixel_per_event * static_cast<double>(layer.totalPixels);
0755   std::poisson_distribution<std::uint64_t> poisson(mean);
0756   const auto requested   = poisson(rng);
0757   const auto maxAttempts = std::max<std::uint64_t>(100, requested * 20);
0758   std::uniform_int_distribution<std::uint64_t> pickLayerPixel(0, layer.totalPixels - 1);
0759 
0760   std::uint64_t created  = 0;
0761   std::uint64_t attempts = 0;
0762   // Step 2: select a uniform layer-wide pixel. The cumulative component counts
0763   // first choose the sensor with probability proportional to its pixel count.
0764   while (created < requested && attempts < maxAttempts) {
0765     ++attempts;
0766     const auto selectedPixel     = pickLayerPixel(rng);
0767     const auto componentPosition = std::upper_bound(layer.cumulativePixels.begin(),
0768                                                     layer.cumulativePixels.end(), selectedPixel);
0769     const auto componentOffset =
0770         static_cast<std::size_t>(componentPosition - layer.cumulativePixels.begin());
0771     const auto& component = m_components[layer.componentIndices[componentOffset]];
0772     const auto cellID     = randomCellID(component, rng);
0773     // Step 3: electronic pixels can fire at most once in an event. At the default
0774     // occupancy, retries due to duplicates are extremely rare.
0775     if (hitMap.contains(cellID)) {
0776       continue;
0777     }
0778 
0779     // This algorithm models pixel occupancy, not the sensor pulse shape.
0780     edm4eic::MutableRawTrackerHit hit;
0781     hit.setCellID(cellID);
0782     hit.setCharge(1.0e6);
0783     hit.setTimeStamp(0);
0784     hitMap.emplace(cellID, hit);
0785     ++created;
0786   }
0787 
0788   if (created < requested) {
0789     warning("RandomNoisePixel '{}': created {}/{} requested hits for detector '{}' layer {}",
0790             name(), created, requested, layer.detectorName, layer.layer);
0791   }
0792 }
0793 
0794 // Event entry point: seed, sample every layer, and emit deterministic cell-ID order.
0795 void RandomNoisePixel::process(const Input& in, const Output& out) const {
0796   auto [outHits]       = out;
0797   const auto [headers] = in;
0798 
0799   if (!m_cfg.addNoise) {
0800     return;
0801   }
0802   if (!headers) {
0803     throw std::runtime_error("RandomNoisePixel requires an EventHeader collection");
0804   }
0805   if (m_layers.empty()) {
0806     throw std::runtime_error("RandomNoisePixel has no cached sensitive geometry");
0807   }
0808 
0809   // Step 1: make the random sequence depend on event identity, not thread scheduling.
0810   std::mt19937_64 rng(seedFromEventHeader(*headers));
0811 
0812   // Step 2: a sorted map removes duplicates and gives stable output ordering.
0813   std::map<std::uint64_t, edm4eic::MutableRawTrackerHit> noiseHits;
0814   for (const auto& layer : m_layers) {
0815     addNoiseHitsForLayer(layer, noiseHits, rng);
0816   }
0817   // Step 3: transfer the completed noise-only collection to PODIO.
0818   for (const auto& [_, hit] : noiseHits) {
0819     outHits->push_back(hit);
0820   }
0821 }
0822 
0823 } // namespace eicrecon