ACTS
Experiment-independent tracking
Loading...
Searching...
No Matches
Graph-Based Track Seeding

Seeding by building and filtering a graph of hit doublets.

Remarks
This page documents GBTS as implemented in ACTS today. GBTS is an alternative to the classical triplet-based Seeding, not a layer on top of it — the two are independent entry points producing the same Acts::SeedContainer.

Why a graph?

Classical ACTS seeding (Seeding) enumerates triplets of space points from a binned grid and cuts on the helix they describe. The combinatorics of that enumeration grow steeply with occupancy, and every triplet is judged in isolation.

GBTS inverts the order. It first builds a graph: nodes are space points, and a directed edge joins two space points on connected detector layers whenever the pair passes a set of cheap two-point cuts. Only then does it look for structure in that graph — long chains of mutually compatible edges — and turn the best chains into seeds. The expensive per-candidate work is therefore done once per edge rather than once per triplet, and the chain length itself becomes a quality signal.

The workflow has four stages, each documented below:

  1. Graph nodes — sort the space points into eta/phi-ordered graph nodes.
  2. Building the graph — create edges between compatible node pairs and link compatible edges to each other.
  3. Chain levels — give every edge a "level", the length of the longest chain of linked edges going outwards from it.
  4. Seed extraction — follow the best fitted path from every edge through a Kalman-like filter and emit seeds.

Geometry and layer connections

GBTS does not use the ACTS tracking geometry. It works on its own lightweight description: a flat list of GbtsLayer logical layers, each subdivided into eta bins.

Acts::Experimental::GbtsLayerDescription gives a layer its ID, its type (barrel or endcap), its sensor technology and its extent. For a barrel layer refCoord is the radius and the bounds are in \(z\); for an endcap it is the other way round. The ID is the caller's own numbering, and the algorithm never decodes it.

A ring of inclined modules is described by its slope in \(r\)- \(z\), along which its eta bins are cut. ActsExamples::GraphBasedSeedingAlgorithm takes the slope from the module planes of the tracking geometry when useLayerTilt is set.

A pixel barrel layer carries one more field, barrelOrder: the position of the layer in the inside-out ordering of the pixel barrel. Every other layer keeps the default -1, so the sign of the field also says whether a layer is a pixel barrel layer. If the caller leaves the field unset, Acts::Experimental::GbtsGeometry sorts the pixel barrel layers by refCoord and fills it in. Set it on every pixel barrel layer or on none of them, because the constructor rejects a partial ordering.

The cuts that were tuned on the pixel barrel read barrelOrder and nothing else: the two innermost-layer cuts of Building the graph ask how deep a layer sits. The \(\tau\) ratio tolerance comes from the layer pairs instead, see below. GBTS therefore runs on any layer numbering.

Which layer pairs may be joined by an edge is a list of Acts::Experimental::GbtsLayerConnection, each naming a source (outer) and a destination (inner) layer. Acts::Experimental::GbtsGeometry combines the layer descriptions with those connections and precomputes, for every pair of connected layers, which eta bin pairs are geometrically compatible with the allowed \(z_0\) range. The result is a bin group list — one inner bin together with all outer bins it may connect to — which serves as the graph builder's iteration schedule, ordered so that outer bins are processed before the inner bins that depend on them. The \(z_0\) range is widened on both ends by GbtsZ0Range::z0Tolerance.

The binning it worked out is readable back off the geometry, so a consumer that runs the same algorithm elsewhere does not have to recompute or pre-generate it: Acts::Experimental::GbtsGeometry::layerBinning gives a layer's Acts::Experimental::GbtsLayerBinning, the eta bins it owns in the global numbering, and Acts::Experimental::GbtsGeometry::binGroups the schedule itself. Eta bins are numbered globally and a layer's are contiguous, so the layers tile the numbering in order. This is what the GPU implementation is configured from.

Note
The connections are trained offline rather than written by hand. Acts::Experimental::GbtsLayerConnectionTool accumulates layer-pair statistics from simulated tracks; the Examples/Scripts/Python/gbts_layer_connection_training_itk.py and gbts_layer_connection_training_odd.py scripts drive it for the ITk and the Open Data Detector. ActsExamples::GraphBasedSeedingAlgorithm reads the resulting table, from a JSON file, and hands the pairs it lists to the geometry.

Graph nodes

Acts::Experimental::GbtsNodeStorage holds the graph nodes. Space points are fed in one at a time through insert, which takes plain scalars rather than an ACTS container:

std::optional<std::uint32_t> insert(SpacePointIndex index, float x, float y,
float z, GbtsLayerIndex layerIndex,
float clusterWidth = 0.f,
float localPositionY = 0.f);

As with Acts::CylindricalSpacePointGrid, an experiment can therefore fill the storage straight from its own space point EDM. Overloads exist for callers that already have \(r\) and \(\phi\), and for an Acts::ConstSpacePointProxy together with the columns carrying the layer index, cluster width and local \(y\) position.

Two different layer numbers meet here, and they are separate types:

Type What it is
GbtsExperimentLayerId the layer id the experiment assign. Sparse and structured – the layer descriptions and connections are written in terms of it. The algorithm treats it as an opaque key.
GbtsLayerIndex where that layer sits in one Acts::Experimental::GbtsGeometry, dense from zero. It indexes the geometry, and it is what a node carries.

insert takes the index, because it is on the per-space-point path. Acts::Experimental::GbtsGeometry::layerIndex hands it out. It is the geometry's own numbering and is not derivable from the id.

insert assigns the node to an eta bin via GbtsLayer::getEtaBin and buffers it. finalize then sorts each bin by \(\phi\) and materialises the nodes into a space point container ordered by eta bin and then by \(\phi\), so that every eta bin is one contiguous range of node indices. A node index is therefore all that the rest of the algorithm needs to pass around.

The per-node data the graph builder reads lives in dynamic columns on that same container. It is packed rather than split into one array per field, because the innermost loop reads all of it together:

struct GbtsNodeParams final {
float minTau{-std::numeric_limits<float>::infinity()};
float maxTau{std::numeric_limits<float>::infinity()};
float phi{};
float r{};
float z{};
};

\(\tau = \cot\theta\). The infinite defaults disable the \(\tau\) cut entirely; only the optional machine-learning lookup table narrows them (see Machine-learning assisted acceptance). Alongside it sits the bookkeeping the graph builder writes:

struct GbtsNodeEdgeInfo final {
std::uint32_t firstEdge{};
std::uint16_t numEdges{};
std::uint16_t isConnected{};
};

Each eta bin carries its node range plus the \(\phi\) index used by the sliding window. The \(\phi\) index duplicates entries shifted by \(\pm 2\pi\) near the wrap-around, so the window never has to handle wrapping:

struct GbtsEtaBinInfo final {
SpacePointIndexRange nodes{0, 0};
std::vector<std::pair<float, SpacePointIndex>> phiNodes;
float minRadius{};
float maxRadius{};
std::int32_t barrelOrder{-1};
GbtsLayerType type{};
GbtsLayerTechnology technology{};
bool empty() const { return nodes.first == nodes.second; }
};

Building the graph

The builder walks the bin groups from Geometry and layer connections. For each inner bin it prepares one sliding window in \(\phi\) per connected outer bin, whose half-width grows with the radial separation of the two bins — the further apart they are, the more a low- \(p_T\) track can bend between them. It then loops over the inner nodes, and for each one scans only the outer nodes inside the window.

A candidate pair \((n_1, n_2)\) becomes an edge if it survives, in order:

Cut Meaning
\(\Delta r > \) minDeltaRadius the two hits are radially separated enough for \(\tau\) to be meaningful
\(\lvert\tau\rvert < \) maxAbsTau within the detector's angular acceptance
\(\tau\) inside both nodes' windows per-node acceptance from the ML lookup table
\(z_0\) inside [minZ0, maxZ0], and \(z\) at the outer radius inside the ROI the pair points back to the luminous region
\(\lvert\kappa\rvert\) below an \(\eta\)-dependent bound consistent with the \(p_T\) threshold

with \(\tau = \Delta z/\Delta r\), \(z_0 = z_1 - r_1\tau\) and the curvature proxy \(\kappa = (\phi_2-\phi_1)/\Delta r\).

Surviving pairs are appended to a flat edge array:

struct GbtsEdge final {
GbtsEdge() = default;
GbtsEdge(SpacePointIndex n1_, SpacePointIndex n2_, float tauRatioCut_,
bool n2PixelBarrel_, float p1_, float p2_, float p3_)
: n1{n1_},
n2{n2_},
n2PixelBarrel{n2PixelBarrel_},
p{p1_, p2_, p3_},
tauRatioCut{tauRatioCut_} {}
SpacePointIndex n1{kSpacePointIndexInvalid};
SpacePointIndex n2{kSpacePointIndexInvalid};
std::uint8_t nNei{0};
bool n2PixelBarrel{};
std::array<float, 3> p{};
float tauRatioCut{};
std::array<std::uint32_t, kGbtsMaxEdgeNeighbours> vNei{};
};

The three fit parameters p are \(\{\exp(-\eta),\ \kappa,\ \phi_1 + \kappa r_1\}\).

Because the inner node's edges are written contiguously, the edges incoming to a node form a contiguous range, recorded in that node's GbtsNodeEdgeInfo. Immediately after creating an edge \((n_1, n_2)\), the builder scans the edges incoming to \(n_2\) — that is, edges \((n_2, n_3)\) — and links the two whenever the implied triplet is consistent: the \(\tau\) ratio, the \(\phi\) continuation and the curvature difference must all agree within tolerance. The \(\tau\) ratio tolerance comes from the layer pairs: a triplet is held to the looser of tauRatioCutInner of its inner pair and tauRatioCutOuter of its outer pair, see Acts::Experimental::GbtsLayerConnection. Acts::Experimental::assignAdaptiveTauRatioCuts fills them from the layers: wider for a pair that skips a pixel barrel layer or, as the outer pair, leaves the pixel barrel, and wider again for a pair with a strip layer. With useSumRatioCut the three differences, each divided by its own cut, may not add up to more than sumRatioCutMax, which rejects a pair that is marginal in all three at once. For pixel-barrel triplets an optional validateTriplets step also fits a circle through the three points and cuts on \(d_0\) and \(p_T\). Each edge stores up to kGbtsMaxEdgeNeighbours (6) such neighbours.

Two further cuts apply on the innermost pixel barrel layers, where the combinatorics are worst. Each cut has its own depth limit on the barrelOrder of the inner layer, and a negative limit switches the cut off:

  • matchBeforeCreate (off by default, limited by matchBeforeCreateMaxBarrelOrder) demands the \(\tau\) half of the triplet test before the edge exists: \(n_2\) must already carry an incoming edge whose \(\tau\) agrees with the candidate's within tauRatioPrecut. A node with two or fewer incoming edges passes unconditionally, there being too little evidence to reject it.
  • Every inner node accumulates a 16-bit \(z_0\) histogram bitmask of its confirmed edges. On the layers down to z0HistogramMaxBarrelOrder that mask rejects candidates whose \(z_0\) falls in an empty bin, and nodes with no connections at all are skipped outright.

Chain levels

With the edge graph built, every edge gets a level: the length of the longest chain of linked edges going outwards from it, so an edge that nothing further out links to is at level 1. An edge only links to edges further in, which are always created after it, so a single pass over the edges in creation order settles every level. Chains longer than maxChainLength edges are left unsettled and take no part in the extraction.

An edge with no settled edge further in exactly one level up is a root, the innermost edge of a chain. An edge further in that is more than one level up belongs to a longer chain outwards elsewhere and does not continue the chain.

Seed extraction

Every edge yields at most one seed candidate: the best fitted path from it inwards to a root, each step going to an edge one level up, i.e. along the longest chain outwards of that root. The paths are found by a depth first search from every edge, fitted on the way by Acts::Experimental::GbtsTrackingFilter, and edges and branches that cannot reach a root far enough in are skipped. Nothing depends on the order the edges are visited in. This is the seed extraction of the traccc GPU GBTS.

The filter is a small Kalman filter over the chain. It carries a state of two independent parts — a quadratic in the bending plane and a linear \(z\) versus \(r\) model — and at each step extrapolates to the next node, forms a \(\chi^2\) residual for each part and rejects the branch if either exceeds its threshold (maxDChi2X, maxDChi2Y).

Every accepted hit adds a fixed reward addHit to the branch score, minus its two \(\chi^2\) increments weighted by weightX and weightY. The score therefore counts the hits on the chain, discounted by how badly they fit the circle and the \(z\) versus \(r\) line. Of the complete paths from an edge, the one with the best score per space point wins, and a path that fails the filter is dropped.

The result is a set of seed candidates. These are reduced in two passes:

  • Clone removal. Candidates are ranked by quality, and each space point is assigned to the best candidate claiming it. A candidate that has lost more than hitShareThreshold of its hits to better candidates is dropped.
  • Seed splitting. Short, central candidates are checked for self-consistency by fitting the circle through three different hit subsets. If the three curvature estimates disagree by more than maxInvRadDiff, the candidate is emitted as two shorter "drop-out" seeds instead of one.

The surviving candidates are written to the output Acts::SeedContainer, with node indices translated back to the caller's own space point indices.

Machine-learning assisted acceptance

When useClusterWidthCuts is enabled, GBTS narrows the per-node \(\tau\) window using a pre-trained lookup table indexed by pixel cluster width. The cluster a track leaves in a pixel module grows with the incidence angle, so the width alone constrains \(\cot\theta\) before any pairing is attempted.

The table carries two sets of bounds per width bin: one for clusters comfortably inside the module, and one for clusters within moduleEdgeTolerance of the module edge, where the cluster may be truncated and the width therefore underestimates the angle. Wide clusters in the pixel endcap are dropped entirely (maxEndcapClusterWidth).

Note
The seeder takes the table itself as tauLookupTable, not a path to it; ActsExamples::GraphBasedSeedingAlgorithm reads it from a JSON file. It is only consulted for pixel barrel layers, and the ACTS examples framework does not currently provide cluster widths or local positions, so this path is exercised only by experiment-side integrations that supply them through insert.

Configuration

The cuts that build and link the doublets live on GbtsGraphBuilder::Config, which GraphBasedSeedingAlgorithm takes as graphConfig and exposes to Python as GbtsGraphBuilderConfig:

Option Stage Effect
minPt Building the graph drives the curvature and \(\phi\)-window bounds
minDeltaRadius, maxAbsTau Building the graph doublet acceptance
minZ0, maxZ0, doubletFilterRZ Building the graph luminous-region cuts on the doublet
cutDPhiMax, cutDCurvMax Building the graph edge-to-edge linking tolerances
useSumRatioCut, sumRatioCutMax Building the graph bound on the summed linking tolerances, against pairs marginal in all three
tauRatioCut Building the graph \(\tau\) scale of the validateTriplets high-pT check. The linking cut comes from Acts::Experimental::GbtsLayerConnection
validateTriplets, d0Max Building the graph circle fit on pixel-barrel triplets
maxEdgesPerSP Building the graph cap on the edge array per space point (30 by default); exceeding it costs efficiency
matchBeforeCreate, tauRatioPrecut, matchBeforeCreateMaxBarrelOrder Building the graph require a compatible incoming edge before creating one, down to that depth in the pixel barrel
z0HistogramMaxBarrelOrder, z0Resolution Building the graph \(z_0\) histogram cut, down to that depth in the pixel barrel

The rest are on GraphBasedTrackSeeder::Config:

Option Stage Effect
nMaxPhiSlice Graph nodes sets the \(\phi\) slice width, and with it the base sliding-window width the graph uses
maxChainLength Chain levels longest chain of edges taken into account
minSeedLevel Seed extraction chain length a candidate must reach
postFitCurvatureFactor Seed extraction tighter curvature bound on the fitted chain, as a factor on the filter's maxCurvature; values below 1 does nothing
addTriplets, maxAbsEtaAddTriplets Seed extraction allow shorter chains within an \(\eta\) range
hitShareThreshold Seed extraction fraction of shared hits above which a candidate is a clone
maxSeedSplitEta, maxInvRadDiff Seed extraction seed splitting
useClusterWidthCuts, tauLookupTable Machine-learning assisted acceptance cluster-width based \(\tau\) windows
maxEndcapClusterWidth, moduleHalfLengthY, moduleEdgeTolerance Machine-learning assisted acceptance cluster-width acceptance and module-edge handling

useStripConnections, which takes the strip layer connections from the connector file instead of the pixel ones, is read where the file is loaded and so sits on GraphBasedSeedingAlgorithm::Config itself.

GbtsTrackingFilter::Config separately controls the chain-following filter of seed extraction:

Option Effect
sigmaX, sigmaY measurement resolution in the bending plane and along \(z\)
maxDChi2X, maxDChi2Y per-step \(\chi^2\) ceilings; a branch exceeding either is dropped
addHit, weightX, weightY the reward and the two \(\chi^2\) weights in the branch score
sigmaMS, radLen multiple-scattering inflation added before each extrapolation
maxCurvature, maxZ0 track-level bounds checked after each update

Implementation pointers

A GPU implementation of the same algorithm, using an equivalent struct-of-arrays layout, lives in the traccc plugin under Traccc/device/common/include/traccc/gbts_seeding.