Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-10-05 08:10:43

0001 // This file is part of the ACTS project.
0002 //
0003 // Copyright (C) 2016 CERN for the benefit of the ACTS project
0004 //
0005 // This Source Code Form is subject to the terms of the Mozilla Public
0006 // License, v. 2.0. If a copy of the MPL was not distributed with this
0007 // file, You can obtain one at https://mozilla.org/MPL/2.0/.
0008 
0009 #include "Acts/Surfaces/SurfaceArray.hpp"
0010 
0011 #include "Acts/Definitions/Algebra.hpp"
0012 #include "Acts/Geometry/GeometryContext.hpp"
0013 #include "Acts/Geometry/Polyhedron.hpp"
0014 #include "Acts/Surfaces/CylinderBounds.hpp"
0015 #include "Acts/Surfaces/Surface.hpp"
0016 #include "Acts/Utilities/AlgebraHelpers.hpp"
0017 #include "Acts/Utilities/Axis.hpp"
0018 #include "Acts/Utilities/Helpers.hpp"
0019 #include "Acts/Utilities/IAxis.hpp"
0020 #include "Acts/Utilities/Ranges.hpp"
0021 #include "Acts/Utilities/ThrowAssert.hpp"
0022 #include "Acts/Utilities/detail/MultiAxisHelper.hpp"
0023 #include "Acts/Utilities/detail/OstreamStateGuard.hpp"
0024 
0025 #include <algorithm>
0026 #include <cmath>
0027 #include <iomanip>
0028 #include <limits>
0029 #include <map>
0030 #include <ranges>
0031 #include <set>
0032 #include <stdexcept>
0033 #include <unordered_set>
0034 #include <utility>
0035 
0036 namespace Acts {
0037 
0038 /// Base interface for all surface lookups.
0039 struct SurfaceArray::ISurfaceGridLookup {
0040   virtual ~ISurfaceGridLookup() = default;
0041 
0042   /// Fill provided surfaces into the contained @c Grid.
0043   /// @param gctx The current geometry context object, e.g. alignment
0044   /// @param surfaces Input surface pointers
0045   virtual void fill(const GeometryContext& gctx,
0046                     std::span<const Surface* const> surfaces) = 0;
0047 
0048   /// Get all surfaces in bin given by the global bin index
0049   /// @param bin the global bin index
0050   /// @return span of surface pointers of the bin at that position
0051   virtual std::span<const Surface* const> at(std::size_t bin) const = 0;
0052 
0053   /// Performs lookup at @c pos and returns bin content as const reference
0054   /// @param gctx The current geometry context object, e.g. alignment
0055   /// @param position Lookup position
0056   /// @param direction Lookup direction
0057   /// @return A span of surface pointers
0058   virtual std::span<const Surface* const> at(
0059       const GeometryContext& gctx, const Vector3& position,
0060       const Vector3& direction) const = 0;
0061 
0062   /// Get all surfaces in bin given by local grid indices and neighbor
0063   /// distance.
0064   /// @param gridIndices the local grid indices
0065   /// @param neighborDistance the neighbor distance to include in the lookup,
0066   ///        per axis
0067   /// @return span of surface pointers of the bin at that position and its neighbors
0068   virtual std::span<const Surface* const> neighbors(
0069       std::array<std::size_t, 2> gridIndices,
0070       std::array<std::uint8_t, 2> neighborDistance) const = 0;
0071 
0072   /// Performs a lookup at @c pos, but returns neighbors as well
0073   /// @param gctx The current geometry context object, e.g. alignment
0074   /// @param position Lookup position
0075   /// @param direction Lookup direction
0076   /// @return A span of surface pointers
0077   virtual std::span<const Surface* const> neighbors(
0078       const GeometryContext& gctx, const Vector3& position,
0079       const Vector3& direction) const = 0;
0080 
0081   /// Returns the total size of the grid (including under/overflow bins)
0082   /// @return Size of the grid data structure
0083   virtual std::size_t size() const = 0;
0084 
0085   /// Gets the center position of bin @c bin in global coordinates
0086   /// @param bin the global bin index
0087   /// @return The bin center
0088   virtual Vector3 getBinCenter(std::size_t bin) const = 0;
0089 
0090   /// Returns copies of the axes used in the grid as @c AnyAxis
0091   /// @return The axes
0092   /// @note This returns copies. Use for introspection and querying.
0093   virtual std::vector<const IAxis*> getAxes() const = 0;
0094 
0095   /// Get the representative surface used for this lookup
0096   /// @return Surface pointer
0097   virtual const Surface* surfaceRepresentation() const = 0;
0098 
0099   /// Checks if global bin is valid
0100   /// @param bin the global bin index
0101   /// @return bool if the bin is valid
0102   /// @note Valid means that the index points to a bin which is not a under
0103   ///       or overflow bin or out of range in any axis.
0104   virtual bool isValidBin(std::size_t bin) const = 0;
0105 
0106   /// The binning values described by this surface grid lookup. They are in
0107   /// order of the axes (optional) and empty for eingle lookups
0108   /// @return Vector of axis directions for binning
0109   virtual std::vector<AxisDirection> binningValues() const { return {}; }
0110 
0111   /// Get the number of local bins in each dimension. This is used to
0112   /// determine the size of the grid for neighbor lookups.
0113   /// @return Array of number of local bins in each dimension
0114   virtual std::array<std::size_t, 2> numLocalBins() const = 0;
0115 
0116   /// Get the bounds on the neighbor window this lookup serves.
0117   /// @return Neighbor window bounds per axis
0118   virtual SurfaceArray::NeighborWindow neighborWindow() const = 0;
0119 };
0120 
0121 namespace {
0122 
0123 struct SingleElementLookupImpl final : SurfaceArray::ISurfaceGridLookup {
0124   explicit SingleElementLookupImpl(const Surface* element)
0125       : m_element({element}) {}
0126 
0127   std::span<const Surface* const> at(std::size_t bin) const override {
0128     if (bin != 0) {
0129       throw std::out_of_range(
0130           "SingleElementLookupImpl only contains one bin with index 0");
0131     }
0132     return m_element;
0133   }
0134 
0135   std::span<const Surface* const> at(
0136       const GeometryContext& /*gctx*/, const Vector3& /*position*/,
0137       const Vector3& /*direction*/) const override {
0138     return m_element;
0139   }
0140 
0141   std::span<const Surface* const> neighbors(
0142       std::array<std::size_t, 2> gridIndices,
0143       std::array<std::uint8_t, 2> neighborDistance) const override {
0144     if (gridIndices != std::array<std::size_t, 2>{0, 0} ||
0145         neighborDistance != std::array<std::uint8_t, 2>{0, 0}) {
0146       throw std::out_of_range(
0147           "SingleElementLookupImpl only contains one bin with zero neighbor "
0148           "distance");
0149     }
0150     return m_element;
0151   }
0152 
0153   std::span<const Surface* const> neighbors(
0154       const GeometryContext& /*gctx*/, const Vector3& /*position*/,
0155       const Vector3& /*direction*/) const override {
0156     return m_element;
0157   }
0158 
0159   std::size_t size() const override { return 1; }
0160 
0161   Vector3 getBinCenter(std::size_t /*bin*/) const override {
0162     return Vector3(0, 0, 0);
0163   }
0164 
0165   std::vector<const IAxis*> getAxes() const override { return {}; }
0166 
0167   const Surface* surfaceRepresentation() const override { return nullptr; }
0168 
0169   void fill(const GeometryContext& /*gctx*/,
0170             std::span<const Surface* const> /*surfaces*/) override {
0171     // no-op: the single element is already fixed at construction time
0172   }
0173 
0174   bool isValidBin(std::size_t bin) const override { return bin == 0; }
0175 
0176   std::array<std::size_t, 2> numLocalBins() const override { return {1, 1}; }
0177 
0178   SurfaceArray::NeighborWindow neighborWindow() const override {
0179     return {{0, 0}, {0, 0}};
0180   }
0181 
0182  private:
0183   std::vector<const Surface*> m_element;
0184 };
0185 
0186 template <class Axis1, class Axis2>
0187 struct SurfaceGridLookupImpl final : SurfaceArray::ISurfaceGridLookup {
0188   SurfaceGridLookupImpl(std::shared_ptr<RegularSurface> representative,
0189                         double tolerance, std::tuple<Axis1, Axis2> axes,
0190                         std::vector<AxisDirection> binValues = {},
0191                         SurfaceArray::NeighborWindow neighborWindow = {},
0192                         std::uint8_t overfill = 0)
0193       : m_representative(std::move(representative)),
0194         m_tolerance(tolerance),
0195         m_axes(std::move(axes)),
0196         m_binValues(std::move(binValues)),
0197         m_neighborWindow(neighborWindow),
0198         m_overfill(overfill) {
0199     if (m_neighborWindow.min[0] > m_neighborWindow.max[0] ||
0200         m_neighborWindow.min[1] > m_neighborWindow.max[1]) {
0201       throw std::invalid_argument("neighbor window floor exceeds its bound");
0202     }
0203 
0204     m_numLocalBins = {std::get<0>(m_axes).getNBins(),
0205                       std::get<1>(m_axes).getNBins()};
0206     m_numGridBins = (m_numLocalBins[0] + 2) * (m_numLocalBins[1] + 2);
0207     m_packStride1 = static_cast<std::size_t>(m_neighborWindow.max[1]) + 1;
0208     m_packStridePerBin =
0209         (static_cast<std::size_t>(m_neighborWindow.max[0]) + 1) * m_packStride1;
0210     m_windowIsFixed = m_neighborWindow.min == m_neighborWindow.max;
0211     if (const auto* cylinder =
0212             dynamic_cast<const CylinderBounds*>(&m_representative->bounds());
0213         cylinder != nullptr) {
0214       m_gridScale = cylinder->get(CylinderBounds::eR);
0215     }
0216   }
0217 
0218   void fill(const GeometryContext& gctx,
0219             std::span<const Surface* const> surfaces) override {
0220     // scratch only: the bin contents survive as the zero-distance packs
0221     FillingGrid fillingGrid(m_numGridBins);
0222 
0223     for (const Surface* surface : surfaces) {
0224       fillSurfaceFootprint(gctx, *surface, fillingGrid);
0225     }
0226 
0227     for (std::vector<const Surface*>& binSurfaces : fillingGrid) {
0228       std::ranges::sort(binSurfaces);
0229       const auto last = std::ranges::unique(binSurfaces);
0230       binSurfaces.erase(last.begin(), last.end());
0231     }
0232 
0233     checkGrid(surfaces, fillingGrid);
0234 
0235     populateNeighborCache(fillingGrid);
0236   }
0237 
0238   std::span<const Surface* const> at(std::size_t globalBin) const override {
0239     if (globalBin >= m_numGridBins) {
0240       throw std::out_of_range("global bin is outside the grid");
0241     }
0242     // the zero-distance pack is the bin content
0243     return surfacePack(globalBin * m_packStridePerBin);
0244   }
0245 
0246   std::span<const Surface* const> at(const GeometryContext& gctx,
0247                                      const Vector3& position,
0248                                      const Vector3& direction) const override {
0249     const std::optional<GridIndex> localBins = findLocalBin2D(
0250         gctx, position, direction, std::numeric_limits<double>::infinity());
0251     if (!localBins.has_value()) {
0252       return {};
0253     }
0254     return surfacePack(neighborPackIndex(*localBins, {0, 0}));
0255   }
0256 
0257   std::span<const Surface* const> neighbors(
0258       std::array<std::size_t, 2> gridIndices,
0259       std::array<std::uint8_t, 2> neighborDistance) const override {
0260     // past the bound the index wraps into the next bin's pack
0261     if (neighborDistance[0] > m_neighborWindow.max[0] ||
0262         neighborDistance[1] > m_neighborWindow.max[1]) {
0263       throw std::out_of_range(
0264           "neighbor distance exceeds the neighbor window bound");
0265     }
0266     // local bins run from the underflow bin 0 to the overflow bin nBins + 1
0267     if (gridIndices[0] > m_numLocalBins[0] + 1 ||
0268         gridIndices[1] > m_numLocalBins[1] + 1) {
0269       throw std::out_of_range("local bin is outside the grid");
0270     }
0271     return surfacePack(neighborPackIndex(gridIndices, neighborDistance));
0272   }
0273 
0274   std::span<const Surface* const> neighbors(
0275       const GeometryContext& gctx, const Vector3& position,
0276       const Vector3& direction) const override {
0277     const std::optional<Crossing> crossing = findCrossing(
0278         gctx, position, direction, std::numeric_limits<double>::infinity());
0279     if (!crossing.has_value()) {
0280       return {};
0281     }
0282 
0283     const GridPoint gridLocal = surfaceToGridLocal(crossing->local);
0284     const GridIndex localBins = localBinsFromPosition2D(gridLocal);
0285 
0286     const GridDistance neighborDistance = crossingNeighborDistance(
0287         gctx, *crossing, direction, gridLocal, localBins);
0288 
0289     return surfacePack(neighborPackIndex(localBins, neighborDistance));
0290   }
0291 
0292   std::size_t size() const override { return m_numGridBins; }
0293 
0294   std::vector<AxisDirection> binningValues() const override {
0295     return m_binValues;
0296   }
0297 
0298   Vector3 getBinCenter(std::size_t bin) const override {
0299     const GeometryContext gctx = GeometryContext::dangerouslyDefaultConstruct();
0300     const GridPoint gridLocal = binCenter(localBinsFromGlobalBin2D(bin));
0301     const Vector2 surfaceLocal = gridToSurfaceLocal(gridLocal);
0302     return m_representative->localToGlobal(gctx, surfaceLocal);
0303   }
0304 
0305   std::vector<const IAxis*> getAxes() const override {
0306     return {&std::get<0>(m_axes), &std::get<1>(m_axes)};
0307   }
0308 
0309   const Surface* surfaceRepresentation() const override {
0310     return m_representative.get();
0311   }
0312 
0313   bool isValidBin(std::size_t globalBin) const override {
0314     const GridIndex indices = localBinsFromGlobalBin2D(globalBin);
0315     return isValidBin(indices);
0316   }
0317 
0318   std::array<std::size_t, 2> numLocalBins() const override {
0319     return numLocalBins2D();
0320   }
0321 
0322   SurfaceArray::NeighborWindow neighborWindow() const override {
0323     return m_neighborWindow;
0324   }
0325 
0326  private:
0327   using GridIndex = std::array<std::size_t, 2>;
0328   using GridPoint = std::array<double, 2>;
0329   /// Neighbor window half width in bins, per axis
0330   using GridDistance = std::array<std::uint8_t, 2>;
0331   /// Offset into @c m_surfacePacks and number of surfaces
0332   using SurfacePackRange = std::pair<std::uint32_t, std::uint32_t>;
0333   /// A ray meeting the representative surface, in both frames
0334   struct Crossing {
0335     Vector2 local;
0336     Vector3 global;
0337   };
0338   /// Bin contents while filling, before they become the zero-distance packs
0339   using FillingGrid = std::vector<std::vector<const Surface*>>;
0340 
0341   /// Hash and equality over a pack read through its range, so the table keeps
0342   /// no second copy. Ranges are offsets and survive the storage growing.
0343   struct PackLookup {
0344     using is_transparent = void;
0345 
0346     const std::vector<const Surface*>* storage = nullptr;
0347 
0348     std::span<const Surface* const> pack(const SurfacePackRange& range) const {
0349       return {storage->data() + range.first, range.second};
0350     }
0351 
0352     std::size_t operator()(std::span<const Surface* const> surfaces) const {
0353       std::size_t hash = surfaces.size();
0354       for (const Surface* surface : surfaces) {
0355         hash ^= std::hash<const Surface*>{}(surface) + 0x9e3779b97f4a7c15ULL +
0356                 (hash << 6) + (hash >> 2);
0357       }
0358       return hash;
0359     }
0360     std::size_t operator()(const SurfacePackRange& range) const {
0361       return (*this)(pack(range));
0362     }
0363 
0364     bool operator()(const SurfacePackRange& lhs,
0365                     const SurfacePackRange& rhs) const {
0366       return std::ranges::equal(pack(lhs), pack(rhs));
0367     }
0368     bool operator()(std::span<const Surface* const> lhs,
0369                     const SurfacePackRange& rhs) const {
0370       return std::ranges::equal(lhs, pack(rhs));
0371     }
0372     bool operator()(const SurfacePackRange& lhs,
0373                     std::span<const Surface* const> rhs) const {
0374       return std::ranges::equal(pack(lhs), rhs);
0375     }
0376   };
0377 
0378   std::shared_ptr<RegularSurface> m_representative;
0379   double m_tolerance{};
0380   // needs to be a tuple for the grid_helper functions
0381   std::tuple<Axis1, Axis2> m_axes;
0382   std::vector<AxisDirection> m_binValues;
0383   SurfaceArray::NeighborWindow m_neighborWindow{};
0384   std::uint8_t m_overfill{};
0385 
0386   // derived from the axes and the window in the constructor
0387   GridIndex m_numLocalBins{};
0388   std::size_t m_numGridBins{};
0389   std::size_t m_packStride1{};
0390   std::size_t m_packStridePerBin{};
0391   bool m_windowIsFixed{};
0392   // a cylinder is binned in the angle but measures the arc length
0393   double m_gridScale = 1.;
0394 
0395   // packs are indexed per (bin, distance along axis 0, distance along axis 1),
0396   // so the index array holds ranges rather than spans to stay affordable
0397   std::vector<const Surface*> m_surfacePacks;
0398   std::vector<SurfacePackRange> m_neighborSurfacePacks;
0399 
0400   /// @pre @p packIndex comes from @c neighborPackIndex with in-range inputs
0401   std::span<const Surface* const> surfacePack(std::size_t packIndex) const {
0402     const SurfacePackRange& range = m_neighborSurfacePacks[packIndex];
0403     return {m_surfacePacks.data() + range.first, range.second};
0404   }
0405 
0406   bool isValidBin(const GridIndex& indices) const {
0407     const GridIndex nBins = numLocalBins2D();
0408     for (std::size_t i = 0; i < indices.size(); ++i) {
0409       const std::size_t idx = indices.at(i);
0410       if (idx <= 0 || idx >= nBins.at(i) + 1) {
0411         return false;
0412       }
0413     }
0414     return true;
0415   }
0416 
0417   GridIndex numLocalBins2D() const { return m_numLocalBins; }
0418 
0419   GridIndex localBinsFromPosition2D(const GridPoint& point) const {
0420     return detail::MultiAxisHelper::getLocalBinsFromPoint(point, m_axes);
0421   }
0422 
0423   GridIndex localBinsFromGlobalBin2D(std::size_t globalBin) const {
0424     return detail::MultiAxisHelper::getLocalBinsFromGlobalBin(globalBin,
0425                                                               m_axes);
0426   }
0427 
0428   std::size_t globalBinFromLocalBins2D(const GridIndex& localBins) const {
0429     return detail::MultiAxisHelper::getGlobalBinFromLocalBins(localBins,
0430                                                               m_axes);
0431   }
0432 
0433   std::size_t neighborPackIndex(const GridIndex& localBins,
0434                                 const GridDistance& neighborDistance) const {
0435     const std::size_t globalGridBin =
0436         detail::MultiAxisHelper::getGlobalBinFromLocalBins(localBins, m_axes);
0437     return globalGridBin * m_packStridePerBin +
0438            neighborDistance[0] * m_packStride1 + neighborDistance[1];
0439   }
0440 
0441   GridPoint binCenter(const GridIndex& localBins) const {
0442     return detail::MultiAxisHelper::getBinCenter(localBins, m_axes);
0443   }
0444 
0445   /// Orthogonal projection of a global point onto the representative surface,
0446   /// in grid coordinates.
0447   std::optional<GridPoint> projectToGrid(const GeometryContext& gctx,
0448                                          const Vector3& position) const {
0449     const Vector3 normal = m_representative->normal(gctx, position);
0450     const std::optional<Crossing> crossing = findCrossing(
0451         gctx, position, normal, std::numeric_limits<double>::infinity());
0452     if (!crossing.has_value()) {
0453       return std::nullopt;
0454     }
0455     return surfaceToGridLocal(crossing->local);
0456   }
0457 
0458   /// Bins traversed by a signed step, preserving its direction across seams.
0459   template <typename Axis>
0460   std::uint8_t axisBinDistance(const Axis& axis, std::size_t from,
0461                                double position, double step) const {
0462     if (step == 0.) {
0463       return 0;
0464     }
0465     const double extent = axis.getMax() - axis.getMin();
0466     if (!std::isfinite(step) || std::abs(step) >= extent) {
0467       return clampValue<std::uint8_t>(axis.getNBins());
0468     }
0469     double endpoint = position + step;
0470     const bool crossesSeam =
0471         endpoint < axis.getMin() || endpoint >= axis.getMax();
0472     if (axis.getBoundaryType() == AxisBoundaryType::Closed) {
0473       // Variable axes wrap indices, not coordinates. Normalize before getBin.
0474       if (crossesSeam) {
0475         endpoint =
0476             axis.getMin() +
0477             std::fmod(std::fmod(endpoint - axis.getMin(), extent) + extent,
0478                       extent);
0479       }
0480     } else {
0481       endpoint = std::clamp(endpoint, axis.getMin(), axis.getMax());
0482     }
0483     const std::size_t nBins = axis.getNBins();
0484     const std::size_t a = std::clamp<std::size_t>(from, 1, nBins);
0485     const std::size_t b =
0486         std::clamp<std::size_t>(axis.getBin(endpoint), 1, nBins);
0487     std::size_t distance = a > b ? a - b : b - a;
0488     if (axis.getBoundaryType() == AxisBoundaryType::Closed) {
0489       distance = step >= 0 ? (b + nBins - a) % nBins : (a + nBins - b) % nBins;
0490       // Returning to the same bin across the seam traversed every other bin.
0491       if (distance == 0 && crossesSeam) {
0492         distance = nBins;
0493       }
0494     }
0495     return clampValue<std::uint8_t>(distance);
0496   }
0497 
0498   /// Below this the track runs along the layer and the window opens fully.
0499   static constexpr double s_minIncidence = 1e-4;
0500 
0501   /// How many bins the track moves along each axis while inside the layer,
0502   /// i.e. between the crossing the lookup uses and the module's projection.
0503   GridDistance crossingNeighborDistance(const GeometryContext& gctx,
0504                                         const Crossing& crossing,
0505                                         const Vector3& direction,
0506                                         const GridPoint& gridLocal,
0507                                         const GridIndex& localBins) const {
0508     const GridDistance maximum = m_neighborWindow.max;
0509 
0510     // no room between floor and bound leaves nothing for the angle to decide
0511     if (m_windowIsFixed) {
0512       return maximum;
0513     }
0514 
0515     if (m_tolerance == 0.) {
0516       return m_neighborWindow.min;
0517     }
0518 
0519     // Polar coordinates are singular at the disc center. Do not evaluate the
0520     // derivative there: even checking its result would raise floating
0521     // exceptions.
0522     if (crossing.local[0] == 0. && m_representative->type() == Surface::Disc) {
0523       return maximum;
0524     }
0525 
0526     const Vector3 normal = m_representative->normal(gctx, crossing.local);
0527     const double incidence = normal.dot(direction);
0528     if (std::abs(incidence) < s_minIncidence) {
0529       return maximum;
0530     }
0531 
0532     // the part of the chord that slides along the layer rather than through it
0533     const double halfPath = m_tolerance / std::abs(incidence);
0534     const Vector3 slide = halfPath * (direction - incidence * normal);
0535 
0536     // the surface's metric maps the slide into what the grid bins
0537     const Vector3 localSlide =
0538         m_representative->localToGlobalTransform(gctx).inverse().linear() *
0539         slide;
0540     const Vector2 boundSlide =
0541         m_representative->localCartesianToBoundLocalDerivative(
0542             gctx, crossing.global) *
0543         localSlide;
0544     // linear, so it maps a step like a position
0545     const GridPoint gridSlide = surfaceToGridLocal(boundSlide);
0546 
0547     GridDistance neighborDistance{};
0548     for (const double side : {-1., 1.}) {
0549       neighborDistance[0] =
0550           std::max(neighborDistance[0],
0551                    axisBinDistance(std::get<0>(m_axes), localBins[0],
0552                                    gridLocal[0], side * gridSlide[0]));
0553       neighborDistance[1] =
0554           std::max(neighborDistance[1],
0555                    axisBinDistance(std::get<1>(m_axes), localBins[1],
0556                                    gridLocal[1], side * gridSlide[1]));
0557       // the window only widens, so at the bound the far end cannot change it
0558       if (neighborDistance[0] >= maximum[0] &&
0559           neighborDistance[1] >= maximum[1]) {
0560         return maximum;
0561       }
0562     }
0563 
0564     return {std::clamp(neighborDistance[0], m_neighborWindow.min[0],
0565                        m_neighborWindow.max[0]),
0566             std::clamp(neighborDistance[1], m_neighborWindow.min[1],
0567                        m_neighborWindow.max[1])};
0568   }
0569 
0570   /// Register a surface in every bin its projection onto the representative
0571   /// surface overlaps. The projected outline gives the bins it passes through,
0572   /// the interior is filled per column, which is exact for a convex outline.
0573   void fillSurfaceFootprint(const GeometryContext& gctx, const Surface& surface,
0574                             FillingGrid& fillingGrid) const {
0575     // the surface has to sit within the layer this grid represents
0576     const Vector3 reference =
0577         surface.referencePosition(gctx, AxisDirection::AxisR);
0578     const Vector3 referenceNormal = m_representative->normal(gctx, reference);
0579     if (!findCrossing(gctx, reference, referenceNormal, m_tolerance)
0580              .has_value()) {
0581       return;
0582     }
0583 
0584     // resolution of the outline: segments per quarter circle for curved
0585     // bounds, and samples per segment, since a straight edge does not project
0586     // to a straight line in grid coordinates
0587     constexpr unsigned int nSamples = 32;
0588 
0589     const Polyhedron polyhedron =
0590         surface.polyhedronRepresentation(gctx, nSamples);
0591 
0592     // columns are keyed by the closed axis, if any, so that the span fill runs
0593     // along an axis whose bins do not wrap
0594     const bool firstAxisClosed =
0595         std::get<0>(m_axes).getBoundaryType() == AxisBoundaryType::Closed;
0596     const std::size_t iColumn = firstAxisClosed ? 0 : 1;
0597     const std::size_t iSpan = firstAxisClosed ? 1 : 0;
0598 
0599     // column bin -> [min, max] bin along the other axis
0600     std::map<std::size_t, std::pair<std::size_t, std::size_t>> columns;
0601     const auto addSample = [&](const Vector3& point) {
0602       const std::optional<GridPoint> gridLocal = projectToGrid(gctx, point);
0603       if (!gridLocal.has_value()) {
0604         return;
0605       }
0606       const GridIndex indices = localBinsFromPosition2D(*gridLocal);
0607       if (!isValidBin(indices)) {
0608         return;
0609       }
0610       const auto [it, inserted] =
0611           columns.try_emplace(indices[iColumn], indices[iSpan], indices[iSpan]);
0612       if (!inserted) {
0613         it->second.first = std::min(it->second.first, indices[iSpan]);
0614         it->second.second = std::max(it->second.second, indices[iSpan]);
0615       }
0616     };
0617 
0618     for (const Polyhedron::FaceType& face : polyhedron.faces) {
0619       for (std::size_t i = 0; i < face.size(); ++i) {
0620         const Vector3& from = polyhedron.vertices.at(face.at(i));
0621         const Vector3& to =
0622             polyhedron.vertices.at(face.at((i + 1) % face.size()));
0623         for (std::size_t k = 0; k < nSamples; ++k) {
0624           const double t = static_cast<double>(k) / nSamples;
0625           addSample(from + t * (to - from));
0626         }
0627       }
0628     }
0629 
0630     for (const auto& [column, span] : columns) {
0631       for (std::size_t i = span.first; i <= span.second; ++i) {
0632         GridIndex indices{};
0633         indices[iColumn] = column;
0634         indices[iSpan] = i;
0635         const int radius = m_overfill;
0636         const auto bins0 = std::get<0>(m_axes).neighborHoodIndices(
0637             indices[0], std::pair{-radius, radius});
0638         const auto bins1 = std::get<1>(m_axes).neighborHoodIndices(
0639             indices[1], std::pair{-radius, radius});
0640         for (const std::size_t bin0 : bins0) {
0641           for (const std::size_t bin1 : bins1) {
0642             if (isValidBin(GridIndex{bin0, bin1})) {
0643               fillingGrid.at(globalBinFromLocalBins2D({bin0, bin1}))
0644                   .push_back(&surface);
0645             }
0646           }
0647         }
0648       }
0649     }
0650   }
0651 
0652   /// Cache the surfaces reachable from every bin at every window size
0653   void populateNeighborCache(const FillingGrid& fillingGrid) {
0654     m_surfacePacks.clear();
0655     m_neighborSurfacePacks.assign(m_numGridBins * m_packStridePerBin, {0, 0});
0656 
0657     std::vector<const Surface*> surfacePack;
0658     const PackLookup packLookup{&m_surfacePacks};
0659     std::unordered_set<SurfacePackRange, PackLookup, PackLookup> packRanges(
0660         0, packLookup, packLookup);
0661     for (std::size_t inputGlobalBin = 0; inputGlobalBin < fillingGrid.size();
0662          ++inputGlobalBin) {
0663       const GridIndex indices = localBinsFromGlobalBin2D(inputGlobalBin);
0664 
0665       if (!isValidBin(indices)) {
0666         continue;
0667       }
0668 
0669       for (int distance0 = 0; distance0 <= m_neighborWindow.max[0];
0670            ++distance0) {
0671         for (int distance1 = 0; distance1 <= m_neighborWindow.max[1];
0672              ++distance1) {
0673           surfacePack.clear();
0674 
0675           const auto span0 = std::get<0>(m_axes).neighborHoodIndices(
0676               indices[0], std::pair<int, int>{-distance0, distance0});
0677           const auto span1 = std::get<1>(m_axes).neighborHoodIndices(
0678               indices[1], std::pair<int, int>{-distance1, distance1});
0679           for (const std::size_t bin0 : span0) {
0680             for (const std::size_t bin1 : span1) {
0681               const std::vector<const Surface*>& binContent =
0682                   fillingGrid.at(globalBinFromLocalBins2D({bin0, bin1}));
0683               std::copy(binContent.begin(), binContent.end(),
0684                         std::back_inserter(surfacePack));
0685             }
0686           }
0687 
0688           std::ranges::sort(surfacePack);
0689           const auto last = std::ranges::unique(surfacePack);
0690           surfacePack.erase(last.begin(), last.end());
0691 
0692           const std::size_t packIndex = neighborPackIndex(
0693               indices, {static_cast<std::uint8_t>(distance0),
0694                         static_cast<std::uint8_t>(distance1)});
0695 
0696           if (const auto it =
0697                   packRanges.find(std::span<const Surface* const>(surfacePack));
0698               it != packRanges.end()) {
0699             m_neighborSurfacePacks[packIndex] = *it;
0700           } else {
0701             throw_assert(m_surfacePacks.size() + surfacePack.size() <=
0702                              std::numeric_limits<std::uint32_t>::max(),
0703                          "surface pack storage exceeds the 32 bit index range");
0704             const SurfacePackRange surfacePackRange = {
0705                 static_cast<std::uint32_t>(m_surfacePacks.size()),
0706                 static_cast<std::uint32_t>(surfacePack.size())};
0707             m_surfacePacks.insert(m_surfacePacks.end(), surfacePack.begin(),
0708                                   surfacePack.end());
0709             packRanges.insert(surfacePackRange);
0710             m_neighborSurfacePacks[packIndex] = surfacePackRange;
0711           }
0712         }
0713       }
0714     }
0715 
0716     m_surfacePacks.shrink_to_fit();
0717   }
0718 
0719   void checkGrid(std::span<const Surface* const> surfaces,
0720                  const FillingGrid& fillingGrid) const {
0721     const std::set<const Surface*> allSurfaces(surfaces.begin(),
0722                                                surfaces.end());
0723 
0724     std::set<const Surface*> seenSurface;
0725     for (const std::vector<const Surface*>& binSurfaces : fillingGrid) {
0726       seenSurface.insert(binSurfaces.begin(), binSurfaces.end());
0727     }
0728 
0729     if (allSurfaces != seenSurface) {
0730       std::set<const Surface*> diff;
0731       std::ranges::set_difference(allSurfaces, seenSurface,
0732                                   std::inserter(diff, diff.begin()));
0733 
0734       throw std::logic_error(std::format(
0735           "SurfaceArray grid does not contain all surfaces provided! "
0736           "{} surfaces not seen",
0737           diff.size()));
0738     }
0739   }
0740 
0741   Vector2 gridToSurfaceLocal(const GridPoint& gridLocal) const {
0742     return {gridLocal[0] * m_gridScale, gridLocal[1]};
0743   }
0744 
0745   GridPoint surfaceToGridLocal(const Vector2& local) const {
0746     return {local[0] / m_gridScale, local[1]};
0747   }
0748 
0749   /// Where a ray meets the representative surface, in both frames.
0750   std::optional<Crossing> findCrossing(const GeometryContext& gctx,
0751                                        const Vector3& position,
0752                                        const Vector3& direction,
0753                                        double tolerance) const {
0754     const Intersection3D intersection =
0755         m_representative
0756             ->intersect(gctx, position, direction,
0757                         BoundaryTolerance::Infinite())
0758             .closest();
0759     if (!intersection.isValid() ||
0760         std::abs(intersection.pathLength()) > tolerance) {
0761       return std::nullopt;
0762     }
0763     const Vector3 global = intersection.position();
0764     return Crossing{
0765         m_representative->globalToLocal(gctx, global, direction).value(),
0766         global};
0767   }
0768 
0769   std::optional<GridIndex> findLocalBin2D(const GeometryContext& gctx,
0770                                           const Vector3& position,
0771                                           const Vector3& direction,
0772                                           double tolerance) const {
0773     const std::optional<Crossing> crossing =
0774         findCrossing(gctx, position, direction, tolerance);
0775     if (!crossing.has_value()) {
0776       return std::nullopt;
0777     }
0778     const GridPoint gridLocal = surfaceToGridLocal(crossing->local);
0779     return localBinsFromPosition2D(gridLocal);
0780   }
0781 };
0782 
0783 std::unique_ptr<SurfaceArray::ISurfaceGridLookup> makeSurfaceGridLookup(
0784     std::shared_ptr<RegularSurface> representative, double tolerance,
0785     std::tuple<const IAxis&, const IAxis&> axes,
0786     SurfaceArray::NeighborWindow neighborWindow, std::uint8_t overfill) {
0787   const auto& [iAxisA, iAxisB] = axes;
0788 
0789   return iAxisA.visit([&]<typename axis_a_t>(const axis_a_t& axisA) {
0790     return iAxisB.visit(
0791         [&]<typename axis_b_t>(const axis_b_t& axisB)
0792             -> std::unique_ptr<SurfaceArray::ISurfaceGridLookup> {
0793           return std::make_unique<SurfaceGridLookupImpl<axis_a_t, axis_b_t>>(
0794               std::move(representative), tolerance,
0795               std::tuple<axis_a_t, axis_b_t>{axisA, axisB},
0796               std::vector<AxisDirection>(), neighborWindow, overfill);
0797         });
0798   });
0799 }
0800 
0801 }  // namespace
0802 
0803 SurfaceArray::SurfaceArray(std::shared_ptr<const Surface> srf)
0804     : m_gridLookup(std::make_unique<SingleElementLookupImpl>(srf.get())),
0805       m_surfaces({std::move(srf)}) {
0806   m_surfacesRawPointers.push_back(m_surfaces.at(0).get());
0807 }
0808 
0809 SurfaceArray::SurfaceArray(const GeometryContext& gctx,
0810                            std::vector<std::shared_ptr<const Surface>> surfaces,
0811                            std::shared_ptr<RegularSurface> representative,
0812                            double tolerance,
0813                            std::tuple<const IAxis&, const IAxis&> axes,
0814                            NeighborWindow neighborWindow,
0815                            std::uint8_t overfill) {
0816   m_gridLookup = makeSurfaceGridLookup(std::move(representative), tolerance,
0817                                        axes, neighborWindow, overfill);
0818   m_surfaces = std::move(surfaces);
0819   m_surfacesRawPointers =
0820       m_surfaces |
0821       std::views::transform(
0822           [](const std::shared_ptr<const Surface>& sp) { return sp.get(); }) |
0823       Ranges::to<std::vector>;
0824   m_gridLookup->fill(gctx, m_surfacesRawPointers);
0825 }
0826 
0827 SurfaceArray::SurfaceArray(SurfaceArray&& other) noexcept = default;
0828 
0829 SurfaceArray& SurfaceArray::operator=(SurfaceArray&& other) noexcept = default;
0830 
0831 SurfaceArray::~SurfaceArray() = default;
0832 
0833 std::span<const Surface* const> SurfaceArray::at(std::size_t bin) const {
0834   return m_gridLookup->at(bin);
0835 }
0836 
0837 std::span<const Surface* const> SurfaceArray::at(
0838     const GeometryContext& gctx, const Vector3& position,
0839     const Vector3& direction) const {
0840   return m_gridLookup->at(gctx, position, direction);
0841 }
0842 
0843 std::span<const Surface* const> SurfaceArray::neighbors(
0844     std::array<std::size_t, 2> gridIndices,
0845     std::uint8_t neighborDistance) const {
0846   return m_gridLookup->neighbors(gridIndices,
0847                                  {neighborDistance, neighborDistance});
0848 }
0849 
0850 std::span<const Surface* const> SurfaceArray::neighbors(
0851     std::array<std::size_t, 2> gridIndices,
0852     std::array<std::uint8_t, 2> neighborDistance) const {
0853   return m_gridLookup->neighbors(gridIndices, neighborDistance);
0854 }
0855 
0856 std::span<const Surface* const> SurfaceArray::neighbors(
0857     const GeometryContext& gctx, const Vector3& position,
0858     const Vector3& direction) const {
0859   return m_gridLookup->neighbors(gctx, position, direction);
0860 }
0861 
0862 std::size_t SurfaceArray::size() const {
0863   return m_gridLookup->size();
0864 }
0865 
0866 Vector3 SurfaceArray::getBinCenter(std::size_t bin) const {
0867   return m_gridLookup->getBinCenter(bin);
0868 }
0869 
0870 std::vector<const IAxis*> SurfaceArray::getAxes() const {
0871   return m_gridLookup->getAxes();
0872 }
0873 
0874 bool SurfaceArray::isValidBin(std::size_t bin) const {
0875   return m_gridLookup->isValidBin(bin);
0876 }
0877 
0878 std::vector<AxisDirection> SurfaceArray::binningValues() const {
0879   return m_gridLookup->binningValues();
0880 }
0881 
0882 std::ostream& SurfaceArray::toStream(const GeometryContext& /*gctx*/,
0883                                      std::ostream& sl) const {
0884   detail::OstreamStateGuard guard{sl};
0885   sl << std::fixed << std::setprecision(4);
0886   sl << "SurfaceArray:" << std::endl;
0887   sl << " - no surfaces: " << m_surfaces.size() << std::endl;
0888 
0889   const std::vector<const IAxis*> axes = m_gridLookup->getAxes();
0890 
0891   for (const auto [j, axis] : enumerate(axes)) {
0892     const AxisBoundaryType bdt = axis->getBoundaryType();
0893     sl << " - axis " << (j + 1) << std::endl;
0894     sl << "   - boundary type: ";
0895     if (bdt == AxisBoundaryType::Open) {
0896       sl << "open";
0897     }
0898     if (bdt == AxisBoundaryType::Bound) {
0899       sl << "bound";
0900     }
0901     if (bdt == AxisBoundaryType::Closed) {
0902       sl << "closed";
0903     }
0904     sl << std::endl;
0905     sl << "   - type: " << (axis->isEquidistant() ? "equidistant" : "variable")
0906        << std::endl;
0907     sl << "   - n bins: " << axis->getNBins() << std::endl;
0908     sl << "   - bin edges: [ ";
0909     const std::vector<double> binEdges = axis->getBinEdges();
0910     for (const auto [i, binEdge] : enumerate(binEdges)) {
0911       if (i > 0) {
0912         sl << ", ";
0913       }
0914       // Do not display negative zeroes
0915       sl << ((std::abs(binEdge) >= 5e-4) ? binEdge : 0.0);
0916     }
0917     sl << " ]" << std::endl;
0918   }
0919   return sl;
0920 }
0921 
0922 const Surface* SurfaceArray::surfaceRepresentation() const {
0923   return m_gridLookup->surfaceRepresentation();
0924 }
0925 
0926 std::array<std::size_t, 2> SurfaceArray::numLocalBins() const {
0927   return m_gridLookup->numLocalBins();
0928 }
0929 
0930 SurfaceArray::NeighborWindow SurfaceArray::neighborWindow() const {
0931   return m_gridLookup->neighborWindow();
0932 }
0933 
0934 std::uint8_t SurfaceArray::maxNeighborDistance() const {
0935   const NeighborWindow window = m_gridLookup->neighborWindow();
0936   return std::max(window.max[0], window.max[1]);
0937 }
0938 
0939 }  // namespace Acts