|
ACTS
Experiment-independent tracking
|
Binned lookup from a position on a layer to the sensitive surfaces near it.
Acts::SurfaceArray answers one question: given where a track crosses a layer, which sensitive surfaces could it hit. It holds the surfaces of one layer in a two-dimensional grid drawn on a representative surface and returns, for a position and a direction, the surfaces registered in the bins around the crossing point.
The figures for the two things that decide whether that works - which bins hold a surface, and how far from the crossing bin the module it hits can be - are on a separate, interactive page: filling and lookup, in figures. Everything below is the written description; the page carries the pictures and the measurements.
A layer holds its sensitive surfaces in a SurfaceArray, and navigation asks the array instead of testing every module:
Both consumers call Acts::SurfaceArray::neighbors with the geometry context, the position and the direction, and receive a std::span of surface pointers. The array does not intersect the surfaces it returns. It narrows the candidates; the caller intersects them.
The single-surface constructor builds an array without a grid. Every lookup returns that one surface.
The grid is drawn on a representative Acts::RegularSurface that the creator derives from the extent of the surfaces:
| Layer | Representative surface | Axis 0 | Axis 1 |
|---|---|---|---|
| Cylinder | Acts::CylinderSurface | phi, Closed | z, Bound |
| Disc | Acts::DiscSurface | r, Bound | phi, Closed |
| Plane | Acts::PlaneSurface | first in-plane direction, Bound | second in-plane direction, Bound |
The axes are Acts::IAxis objects, equidistant or variable, and every lookup implementation is templated on both axis types behind the ISurfaceGridLookup interface. The boundary type (Acts::AxisBoundaryType) fixes what happens at the edge. A Closed axis wraps: the bin after the last phi bin is the first, and a neighborhood of an edge bin continues on the other side. A Bound axis clamps: a position past the edge lands in the edge bin and a neighborhood stops there. Each axis also carries an underflow and an overflow bin, so Acts::SurfaceArray::size returns (n0 + 2) * (n1 + 2).
Bin edges are expressed in the local frame of the representative surface. On a cylinder and a disc the creator rotates that frame so that the phi seam at +-pi falls between modules, never on a module center.
The grid is binned in the quantities the axes name. The bound-local coordinates of the representative surface are not always those: a Acts::CylinderSurface measures arc length R * phi along its first local coordinate while the grid bins phi. surfaceToGridLocal and gridToSurfaceLocal divide and multiply by the cylinder radius; on a disc and a plane they are the identity. Both maps are linear, so they carry a displacement the same way as a position.
The array registers every surface in every bin its projection onto the representative surface overlaps. fillSurfaceFootprint does this once per surface at construction:
The footprint is sampled, so sufficiently fine bins can be missed between samples. Filling the span can also include extra cells for a concave outline. Keying the columns by the axis that wraps prevents a span across the phi seam: a module straddling +-pi produces columns at both ends, each with its own span along the bound axis.
The optional overfill argument of Acts::SurfaceArray, Acts::SurfaceArrayCreator and Acts::LayerCreator expands every matched cell during construction. It defaults to zero. A value of one also fills the immediate neighbors, including diagonals; two reaches the next neighbors, and n reaches all cells within n bin steps on each axis. Closed axes wrap, and expansion stops at nonperiodic grid edges. Underflow and overflow cells remain empty. Overlapping contributions are deduplicated.
Overfilling changes the stored contents returned by at() as well as neighbors(). It is independent of NeighborWindow: the latter gathers those contents around the lookup cell. Overfilling can provide a margin for alignment or sampling gaps, at the cost of more candidates; it does not guarantee that a sampled footprint covers arbitrarily fine grids.
A module projected onto a disc grid, the two point tests the footprint fill replaced, and what each of them costs in bins are in the figures.
After all surfaces are filled the bin contents are sorted and deduplicated, checkGrid verifies that every input surface landed in at least one bin, and the neighbor cache is built. The filling grid is scratch; its contents survive as the zero-distance packs of the cache.
neighbors(gctx, position, direction) is the navigation entry point:
at(gctx, position, direction) skips step 3 and returns the content of the crossing bin alone.
The lookup happens where the track crosses the representative surface; a module is registered where it projects onto that surface. Between the two the track moves along the layer, the further the shallower the crossing, and the window around the crossing bin spans that movement.
crossingNeighborDistance derives the slide from the crossing geometry:
The derivative supplies the local metric. This is a first-order estimate on curved surfaces, bounded by the configured window. At a coordinate singularity, an undefined displacement opens the affected axis to its configured bound.
The same crossing, in r-z and on the bin grid, and the miss rate each window policy carries across the barrel, are in the figures.
Below an incidence |n . d| of 1e-4 the track runs along the layer and the window opens to its bound.
Acts::SurfaceArray::NeighborWindow bounds the distance per axis:
The computed distance is clamped into [min, max] on each axis. max also sizes the neighbor cache, so it is a cost as well as a bound. The defaults set by Acts::SurfaceArrayCreator and Acts::LayerCreator are:
| Layer | max | Meaning |
|---|---|---|
| Cylinder | {1, 2} | one bin in phi, two in z |
| Disc | {2, 1} | two bins in r, one in phi |
| Plane | {2, 2} | two bins in either direction |
min is {0, 0} everywhere. The floor exists for lookups under a geometry context the fill did not see. The fill runs once, in the construction context; a lookup under an alignment context sees moved surfaces, and a floor of one bin keeps them reachable. {1, 1} for both min and max reproduces the fixed 3x3 lookup the array used before the window was sized by the crossing angle. When min equals max the angle is not evaluated.
Constructing a NeighborWindow from a single std::uint8_t is deprecated; it sets max to that value on both axes and min to one, or to zero if the bound is zero. Acts::SurfaceArray::maxNeighborDistance is deprecated in favor of Acts::SurfaceArray::neighborWindow.
The set of surfaces reachable from a bin at a given distance is fixed once the array is filled. populateNeighborCache precomputes it for every valid bin and every (distance0, distance1) up to max. Each result is a sorted, deduplicated pack of surface pointers. Identical packs are stored once, found through a hash table that reads packs back through their offsets, and the index array holds (offset, count) pairs of 32-bit integers.
A lookup is therefore one index computation, bin * stridePerBin + distance0 * stride1 + distance1, and a span into the pack storage. No gather over neighboring bins happens at lookup time.
The index array has (n0 + 2) * (n1 + 2) * (max0 + 1) * (max1 + 1) entries. Both this index and the pack contents cost memory: each pack lists the surfaces of up to (2 max0 + 1) * (2 max1 + 1) bins. On the generic detector at the default bounds the cache is 13.6 MB. A lookup on a toy barrel costs about 80 ns; what that is made of is in the figures.
SurfaceArray is also used by Acts::SurfaceArrayNavigationPolicy in Gen3 geometry.