← Surface array reference

ACTS · Acts::SurfaceArray

Filling a layer, and finding the module again

A surface array bins the modules of one layer on a representative surface, and answers, for a track crossing it, which of them the track could hit. Two questions decide whether that works: which bins hold a surface, and how far from the crossing bin the module it hits can be.

The figures below are live. The prose that surrounds them is deliberately thin — the written description of the class, its axes, its bounds and its cache is in the reference page.


Which bins hold a surface

Registration is an area question: does this module overlap this bin? The fill that preceded fillSurfaceFootprint answered it with two point tests. Surface → grid: project the module's reference position, register that one bin. Grid → surface: flood-fill outward from it, keeping every bin whose centre projects onto the module. One direction handles a module smaller than a bin; the other handles a module larger than a bin, and the intuition was that they covered each other.

They do not. The first samples one point of the module against the grid, the second samples one point of the bin against the module, and two point tests do not compose into a set-overlap test. What the flood fill computes is the true footprint eroded by up to half a bin on each axis — and a module can be smaller than a bin on one axis while spanning many bins on the other, which is where the erosion stops being a boundary ring and starts swallowing the surface whole.

how a bin qualifies
span of column 9 seam projected module outline seam: columns 0 and 13 hold their own spans -pi pi phi (axis 1, Closed) 400 200 r tinted: bins the surface is registered in; dots: bin centers inside the outline
A trapezoidal module on a disc layer, binned in (r, phi): phi across, r up, the seam at ±pi ruled at both ends. The edges look curved because they are straight in space — a straight chord at perpendicular distance d from the beam line is r = d / cos(phi − phi₀) in these coordinates, so the inner and outer edges both bow toward smaller r at their midpoint. That is why the outline has to be sampled rather than taken from the vertices. Switch the test above: dashed red is a bin the module really overlaps that a bin-centre predicate throws away.

The three fills side by side, on the same module. Drag it inside any panel, or use the sliders; the presets walk through the three regimes.

Regime
Bounds

1 · Surface → grid

fillSurfaceToBinMapping
the reference position's bin, and nothing else

2 · + grid → surface

fillBinToSurfaceMapping
flood fill, keep bins whose centre is on the module

3 · Projected footprint

fillSurfaceFootprint
sample the outline, span-fill each phi column

registered, really overlapped overlapped but not registered registered without overlap · bin centre   ✛ reference position

What this view simplifies: it shows a phi sector with hard edges, so a module pushed off the side is clipped. The real phi axis is Closed and a sample past ±pi wraps to the bin on the other side. The r axis is Bound, where the wrap clamps into the edge bin rather than dropping the sample, which is why dragging past the top or bottom leaves no holes.

What ±1 at lookup time can and cannot repair

A lookup that widens every query to the neighbouring bins repairs a half-bin erosion, because the lost bins are all one step from a kept one — and it charges 3×3 candidates on every query to do it. It cannot repair a gap deeper than the expansion, and in the anisotropic regime the gap is not one bin.


How finely the outline is sampled

The footprint fill walks the surface's polyhedron. That polyhedron approximates a curved edge with straight segments on a global phi grid of 4 × quarterSegments steps, so at quarterSegments = 1 a full ring is a square inscribed in the circle and the fill loses everything outside it. Planar bounds — rectangle, trapezoid — ignore the argument entirely, which is why the generic detector never showed this.

quarterSegments · full ring, r 200–300 mm, unrolled in phi

quarterSegmentsoutline falls short by

Measured against a converged outline, per phi, for a full ring of r 200–300 mm. A radial sector of ±0.15 rad loses 0.84 mm at q = 1 through q = 8 alike — its phi range contains no multiple of pi/2, so nothing is added until the global grid is fine enough to drop a point inside it.

Spanning a column assumes an unbroken interval in r; a concave projection can include extra cells. Sampling can also miss cells between outline points on sufficiently fine grids. The optional overfill construction parameter expands each matched cell by a radius in bins: zero preserves the footprint, one includes immediate neighbors and diagonals, and n reaches n cells along each axis. It wraps the phi seam and stops at nonperiodic grid edges.

On the generic detector, across all 48 layers against brute-force truth: hits lost at |eta| = 3.5 go from 4.9% to zero. With the ±1 expansion still in place that costs 9.0 → 25.0 candidates per query at eta = 0. With the expansion switched off, the same scan gives 3.9 candidates on the disc and 9.0 on the barrel with nothing lost: today's cost, for strictly better coverage.


How far the track slides

A barrel layer is not a cylinder. Its modules sit staggered either side of one, and the array bins them by projecting each module radially onto the representative cylinder in between. A lookup intersects that same cylinder and reads the bin it lands in.

Those two points are not the same point. A track crossing at an angle enters the layer, travels a chord through its thickness and leaves somewhere else in z. At normal incidence the displacement is nothing; at eta = 3 the chord is ten times the layer half-thickness and the crossing bin no longer holds the module the track hits. The window around the crossing bin is what closes the gap, and the question is how wide it has to be.

Everything below is computed from the controls, not quoted: z(r) = r sinh eta, so the slide from the representative surface to a module offset by Δr is Δr sinh eta, and the chord the window is sized from is tolerance × sinh eta.

neighbor window on both views
window: 2 bins each side tolerance representative cylinder registered footprint modules at two radii track half chord = tolerance / |n.d| crossing hit slide slide r z bins 2 to 6 are served; the module the track hits is registered in bins 5 to 7
Staggered modules either side of the representative cylinder. The track crosses the cylinder in one bin and reaches the module it hits in another; the chord through the layer thickness is what separates them.
footprint of the hit module crossing bin hit window: 2 in z, 0 in phi the crossing bin alone would miss the module; the window in z reaches it 0 1 2 3 4 5 6 7 8 9 10 6 5 4 3 2 1 0 z bin (axis 1, Bound) phi bin (axis 0, Closed)
The same crossing on the bin grid. The window opens along z only — a straight track from the beam line barely moves in the angular coordinate, and widening that axis would only multiply candidates.

Vary it

Those two are one crossing at one angle. Below, the same geometry with the angle, the stagger, the binning and the module length in your hands, and three window policies scored against the modules the track actually crosses.

Crossing bin only

no window

Fixed ±1 bin

what main serves — #5186's 1/|n·d| term is capped at 1 by every caller, so it never fires

Sized by the slide

bins spanned by ±tolerance sinh eta, clamped to NeighborWindow max 2 in z

bin served by the lookup served, holds nothing the track hits holds the hit module, not served ● crossing   ◆ where the track meets the module

The stagger and the tolerance are tied, as they are in the creator: SurfaceArrayCreator takes the tolerance from protoLayer.range(AxisR) * 0.5, so it is the stagger plus the module half-thickness. That is why the window always covers the slide it needs to — the same radial extent sets both.


Across the barrel

One eta proves nothing. Sweeping the whole range at the current stagger and binning gives the miss rate each policy carries, and what it charges in candidates to get there.

policymissescandidates

Miss rate over 400 eta samples from 0 to 3.5, mean candidates per query over the same. A miss is a track whose module is registered only in bins the lookup never reads — the hit is unrecoverable, no later stage sees it.


What it costs

Measuring the slide is not free. As first written it went through two full Surface::intersect calls to project the ends of the chord, and that tripled the price of a lookup. Taking the same step from localCartesianToBoundLocalDerivative instead — the reference frame with the curvilinear metric already folded in — costs one Jacobian apply.

variantns / lookupsurfaces returned
before, fixed ±17614.2
window from two projections2453.14
window from the surface metric1283.14
+ Jacobian without the angle1103.14
+ grid scale resolved once1043.14
+ rigid placement inverses803.14

Toy barrel, 60 × 20 bins, 3.28M lookups, RelWithDebInfo with assertions forced on. The narrower window returns 4.5× fewer candidates, and each one it drops is an intersection the caller does not run — so even the 245 ns version paid for itself. The remaining 80 ns is roughly what the fixed window cost before any of this.

What this simplifies: one phi slice, straight rays from the origin, and a barrel of identical modules on two radii. A real track curves and starts off-axis, which moves the angular axis too — the reason the window's phi bound is 1 rather than 0 even though a straight ray from the origin never needs it. The disc case swaps the axes: the slide runs in r, and the same metric returns (dr, dphi) without a per-surface-type case.


← Surface array reference