Warning, /acts/docs/groups/gsf.md is written in an unsupported language. File is not indexed.
0001 @defgroup gsf The Gaussian Sum Filter
0002 @ingroup track_fitting
0003 @brief Multi-component fitter for electrons with non-Gaussian energy loss
0004
0005 > [!tip]
0006 > This page documents the @ref Acts::GaussianSumFitter "GSF" as implemented in
0007 > ACTS today. For the pointwise multiple-scattering and ionization formalism that
0008 > the GSF shares with the ordinary Kalman filters, see @ref kf_material_effects;
0009 > for the high-level conceptual picture see @ref material-eff. The reference
0010 > implementation and the tuning studies quoted below are described in detail in
0011 > @cite Huth:2024.
0012
0013 ## Why a mixture of Gaussians?
0014
0015 The @ref Acts::KalmanFitter "Kalman Filter" (KF) is the optimal estimator only as
0016 long as every involved distribution is Gaussian. For electrons this breaks down:
0017 their dominant energy loss is **bremsstrahlung**, whose Bethe–Heitler
0018 distribution is strongly non-Gaussian and heavily tailed. Feeding that through
0019 the single-Gaussian pointwise update of the KF (see the
0020 @ref kf-material-eloss "energy-loss note") biases the momentum estimate and
0021 wrecks its error estimate.
0022
0023 The **Gaussian Sum Filter** (GSF) @cite Fruhwirth:1997 addresses this by
0024 modelling the track state as a *weighted mixture* of Gaussians instead of a
0025 single one,
0026
0027 @f[
0028 p(\vec x) = \sum_i^{N_c} w_i\, \mathcal N(\vec x \mid \vec\mu_i, \mathbf\Sigma_i),
0029 \qquad \sum_i^{N_c} w_i = 1 ,
0030 @f]
0031
0032 and running, in effect, one Kalman filter per component. Each component carries a
0033 weight, a bound parameter vector and a bound covariance:
0034
0035 @snippet{trimleft} GsfComponent.hpp gsf component
0036
0037 > [!note]
0038 > The GSF is substantially more expensive than the KF and is therefore typically
0039 > only run when a track is likely to be an electron — usually to re-fit a
0040 > silicon track that has been associated with an electromagnetic-calorimeter
0041 > cluster @cite Huth:2024. The GSF itself is a *fitter* — its measurement sequence is
0042 > taken as given — but the same Bethe–Heitler mixture machinery can also run
0043 > *inside* the CKF during track finding to recover electron tracks; see
0044 > @ref gsf-ckf-brem.
0045
0046 ## Bethe–Heitler energy loss as a mixture {#gsf-bethe-heitler}
0047
0048 On a material surface the GSF replaces the KF's deterministic ionization loss and
0049 Landau straggling on @f$q/p@f$ with an explicit mixture model of the
0050 Bremsstrahlung loss. The Bethe–Heitler probability density of the energy
0051 retention @f$z = E_f/E_i@f$ depends only on the traversed thickness in radiation
0052 lengths @f$x/X_0@f$,
0053
0054 @f[
0055 f(z) = \frac{[-\ln z]^{c-1}}{\Gamma(c)}, \quad 0 \le z \le 1,
0056 \qquad c = \frac{x/X_0}{\ln 2} .
0057 @f]
0058
0059 This is approximated by a 1D Gaussian mixture in @f$z@f$,
0060 @f$f(z) \approx \sum_n^{N_{bh}} \pi_n\,\mathcal N(z \mid \mu_{z,n}, \sigma_{z,n})@f$.
0061 For a **single** component the natural choice keeps the first two moments of the
0062 exact distribution @cite Huth:2024,
0063
0064 @f[
0065 \mu_z = e^{-t}, \qquad \sigma_z^2 = 3^{-c} - 4^{-c}, \qquad t = x/X_0 ,
0066 @f]
0067
0068 which is what @ref Acts::BetheHeitlerApproxSingleCmp evaluates:
0069
0070 @snippet{trimleft} BetheHeitlerApprox.hpp single component moments
0071
0072 A single Gaussian reflects the true, tailed distribution very poorly (see
0073 @ref fig_gsf_bethe_heitler "the figure below"), so in practice a multi-component
0074 approximation is used. Because the mixture cannot be derived in closed form, its
0075 weights, means and variances are pre-fitted (minimising either the
0076 Kullback–Leibler divergence or the CDF distance to @f$f(z)@f$) and stored as
0077 polynomials in @f$x/X_0@f$ so they can be interpolated at run time. Any
0078 approximation is accessed through the abstract interface:
0079
0080 @snippet{trimleft} BetheHeitlerApprox.hpp bethe heitler interface
0081
0082 `mixture()` writes @f$N_{bh}@f$ one-dimensional components (weight, mean, variance
0083 in @f$z@f$) into a caller-provided span:
0084
0085 @snippet{trimleft} BetheHeitlerApprox.hpp gaussian component
0086
0087 @anchor fig_gsf_bethe_heitler
0088
0089 {width=450px}
0090
0091 The concrete @ref Acts::PolynomialBetheHeitlerApprox implements the polynomial
0092 form; the default parametrisation shipped in the source
0093 (@ref Acts::makeDefaultBetheHeitlerApprox) and the reference JSON configuration
0094 used by the examples (`betheHeitler_geantSim_cdf_nC6_O5.json`) both use a
0095 **six-component, fifth-order** CDF fit, split into a low- and a high-thickness
0096 range at @f$x/X_0 = 0.1@f$ @cite Huth:2024. When a surface exceeds the valid
0097 @f$x/X_0@f$ range the fitter counts the occurrence and emits a warning; the
0098 deprecated alias `Acts::AtlasBetheHeitlerApprox` remains for the ATLAS/Athena
0099 `.par` data format.
0100
0101 Applying the loss convolves every track-state component with every Bethe–Heitler
0102 component, so a mixture of @f$N_c@f$ components becomes @f$N_c \cdot N_{bh}@f$. In
0103 the backward pass an effective energy *gain* is applied instead.
0104
0105 ## The algorithm on a surface {#gsf-algorithm}
0106
0107 The GSF actor (`Acts::detail::Gsf::GsfActor`) drives the fit as a propagator
0108 actor. When the multi-stepper reports the state on a surface, the actor executes
0109 the following, in this order (see also @ref fig_gsf_overview "the figure below"):
0110
0111 1. Transport each component's covariance onto the surface.
0112 2. If the surface has material, apply **multiple scattering** as `PreUpdate`
0113 (measurement surface) or `FullUpdate` (passive surface) — this reuses exactly
0114 the pointwise machinery documented in @ref kf-material-scattering.
0115 3. Perform the **Kalman update**: run the measurement update for every component
0116 and re-weight them. Up to a normalisation, a component's weight is scaled by
0117 the likelihood of the measurement given that component,
0118 @f[
0119 w_{k|k}^i \;\propto\; w_{k|k-1}^i \,
0120 \mathcal N\!\bigl(m_k \mid \mathbf H_k \vec x_{k|k-1}^i,\;
0121 \mathbf V_k + \mathbf H_k \mathbf\Sigma_{k|k-1}^i \mathbf H_k^{\mathsf T}\bigr),
0122 @f]
0123 i.e. components incompatible with the measurement are exponentially
0124 suppressed. On a passive surface a no-measurement update is done instead
0125 (which may flag a hole). Concretely, the weight is multiplied by
0126 @f$\sqrt{1/\det R}\,\exp(-\tfrac12\chi^2)@f$, with the smallest
0127 @f$\chi^2@f$ over the components factored out for numerical stability and
0128 the weights normalised afterwards:
0129 @snippet{trimleft} GsfUtils.hpp posterior weights
0130 4. Apply the **Bethe–Heitler** convolution of @ref gsf-bethe-heitler to every
0131 component, expanding the mixture.
0132 5. **Reduce** the mixture back down (see @ref gsf-reduction) and drop components
0133 below the weight cutoff.
0134 6. Push the reduced mixture back into the stepper, then apply the `PostUpdate`
0135 scattering on measurement surfaces.
0136
0137 > [!note]
0138 > The measurement update (step 3) always happens **before** the
0139 > Bremsstrahlung loss (step 4): the update must use the state estimated from
0140 > the material seen between the *previous* and the *current* surface. The mixture
0141 > reduction (step 5) runs only *after* the loss, because the Kalman update
0142 > cannot increase the component count — only the convolution can.
0143
0144 @anchor fig_gsf_overview
0145
0146 {width=450px}
0147
0148 ## Mixture reduction {#gsf-reduction}
0149
0150 Left unchecked, the component count would grow by a factor @f$N_{bh}@f$ per
0151 material surface. To keep it bounded, a reducer is invoked after each convolution
0152 to bring the mixture down to `min(stepper.maxComponents, maxComponents)`
0153 components. The reducer is a delegate, so it can be swapped out:
0154
0155 @snippet{trimleft} GsfOptions.hpp mixture reducer
0156
0157 Two production reducers are provided (a third, `…Naive`, is a
0158 reference/benchmark baseline):
0159
0160 - @ref Acts::reduceMixtureLargestWeights — simply discards the lowest-weight
0161 components. Fast, but loses information.
0162 - @ref Acts::reduceMixtureWithKLDistance — greedily merges the pair of components
0163 with the smallest symmetric Kullback–Leibler distance (evaluated on the
0164 @f$q/p@f$ dimension only) until the target count is reached. Slower, but
0165 markedly better, and the recommended choice @cite Huth:2024.
0166
0167 The pairwise distance driving that greedy merge is the symmetric KL divergence,
0168 restricted to the @f$q/p@f$ dimension:
0169
0170 @snippet{trimleft} GsfComponentMerging.cpp kl divergence
0171
0172 ## Mixture merging {#gsf-merging}
0173
0174 *Mixture merging* (or *component merging*) collapses a mixture into a single
0175 parameter vector and covariance — as opposed to the mixture *reduction* above,
0176 which brings a mixture down to a smaller mixture. Several steps need it: storing
0177 an intermediate state, and producing the final fitted parameters. The method is
0178 selectable via the @ref Acts::ComponentMergeMethod enum:
0179
0180 @snippet{trimleft} GsfOptions.hpp component merge method
0181
0182 - `eMean` keeps the first two moments (weighted mean and covariance of the
0183 mixture). The mean is a poor summary of a tailed distribution and can bias the
0184 result.
0185 - `eMaxWeight` (the default) takes the parameters of the highest-weight
0186 component as the point estimate while still reporting the full mixture
0187 covariance. When one component dominates, this approximates the mode well and
0188 avoids the bias.
0189
0190 Merging must respect cyclic bound coordinates. Which coordinates are cyclic
0191 depends on the surface type, encoded as compile-time angle descriptions (note
0192 that on a cylinder the local @f$R\phi@f$ coordinate is cyclic, scaled by the
0193 radius):
0194
0195 @snippet{trimleft} GsfComponentMerging.hpp angle description
0196
0197 The mean itself is then formed with complex-phase arithmetic — each cyclic
0198 coordinate is mapped onto the unit circle, averaged as a complex number, and
0199 converted back with `std::arg` — so that angles wrap correctly:
0200
0201 @snippet{trimleft} GsfComponentMerging.hpp circular mean
0202
0203 ## Multi-component transport {#gsf-multistepper}
0204
0205 Each component must be transported individually, so the GSF runs on the
0206 @ref Acts::MultiEigenStepperLoop rather than the single-component stepper. The
0207 navigator, however, must see a *single* trajectory. The stepper therefore
0208 presents a reduced representation to the navigation, configurable through the
0209 reducer type; the default is the highest-weight component
0210 (`Acts::MaxWeightReducerLoop`, with `Acts::MaxMomentumReducerLoop` as an
0211 alternative), which keeps the navigation stream close to the bulk of the mixture.
0212
0213 Determining when the whole multi-component state has "reached" a surface is
0214 handled by @ref Acts::MultiStepperSurfaceReached, which by default treats the
0215 state as on-surface once its *average* is within tolerance. This guards against a
0216 pathology described in @cite Huth:2024 — low-momentum components approaching a
0217 cylinder on a straight-line intersection can spiral indefinitely while always
0218 reporting *reachable*. A step limit that engages once the first component lands
0219 on the surface (`stepLimitAfterFirstComponentOnSurface`, default 50) forces the
0220 remaining stragglers to *unreachable* and removes them, after which the weights
0221 are renormalised:
0222
0223 @snippet{trimleft} MultiStepperLoop.ipp step limit
0224
0225 ## Forward/backward passes and output {#gsf-passes}
0226
0227 @ref Acts::GaussianSumFitter is constructed from a propagator, a shared
0228 Bethe–Heitler approximation and a logger, and exposes two `fit` overloads: one
0229 for the standard @ref Acts::Navigator, and one taking an explicit surface
0230 sequence for use with the @ref Acts::DirectNavigator — the latter is the
0231 re-fitting entry point used in the electron workflow above.
0232
0233 A fit runs a forward pass from the start parameters, then a backward pass that
0234 starts from the last measurement with its covariance inflated by
0235 `reverseFilteringCovarianceScaling` (default 100) and targets the reference
0236 surface. Measurement surfaces that were seen going forward but not on the way
0237 back are flagged as outliers. The multi-component state is merged
0238 (@ref gsf-merging) into the single set of parameters that downstream algorithms
0239 expect; the full final mixture can optionally be attached to the track.
0240
0241 > [!note]
0242 > The current implementation stores only the **means** of the per-surface states
0243 > in the @ref Acts::MultiTrajectory, so it performs **no** dedicated component
0244 > smoothing of the kind originally described for the GSF — the backward pass
0245 > plays the role of the smoother.
0246
0247 ## Configuration and tuning {#gsf-configuration}
0248
0249 The knobs on @ref Acts::GsfOptions trade physics performance against runtime. The
0250 values below summarise the scan in @cite Huth:2024; the ACTS example chain uses
0251 12 components, KL-distance reduction, `eMaxWeight` merging and a weight cutoff of
0252 @f$10^{-4}@f$.
0253
0254 | Option | Effect | Guidance @cite Huth:2024 |
0255 | --- | --- | --- |
0256 | `maxComponents` | mixture size after each reduction | runtime grows @f$\approx@f$ quadratically; physics plateaus beyond @f$\sim 12@f$ (library default 4, example default 12) |
0257 | `weightCutoff` | discard components below this weight | @f$10^{-4}@f$ is a good default; @f$0.1@f$ is too aggressive (fit failures spike) |
0258 | `mixtureReducer` | reduction algorithm | KL-distance clearly beats the weight cut at modest extra cost |
0259 | `componentMergeMethod` | mixture → single estimate | `eMaxWeight` avoids the @f$q/p@f$ bias seen with `eMean` |
0260 | Bethe–Heitler approx | mixture model of the loss | 6-component CDF polynomials, split at @f$x/X_0=0.1@f$ |
0261 | `reverseFilteringCovarianceScaling` | covariance inflation for the backward pass | default 100 (not tuned for all setups) |
0262 | `disableAllMaterialHandling` | switch off convolution and scattering | debugging only |
0263
0264 The payoff: against the KF, the 12-component GSF turns a heavily one-sided
0265 @f$q/p@f$ residual into a near-symmetric one and shrinks its width, while the KF's
0266 @f$q/p@f$ pull — its error estimate — is badly distorted by the non-Gaussian loss
0267 @cite Huth:2024. A single-component GSF (equivalent to a KF using the Bethe–Heitler
0268 mean and variance) is visibly biased, which is what motivates the mixture in the
0269 first place.
0270
0271 ## Bremsstrahlung recovery in the CKF {#gsf-ckf-brem}
0272
0273 The per-component Bethe–Heitler application of @ref gsf-bethe-heitler is not
0274 exclusive to the fitter. The @ref Acts::CombinatorialKalmanFilter can optionally
0275 run in a **bremsstrahlung-recovery** mode that reuses the same machinery to find
0276 electron tracks that a single-component filter would otherwise lose to a large,
0277 non-Gaussian energy loss.
0278
0279 The mode is selected purely by the stepper type. When the CKF is built over a
0280 multi-component stepper (@ref Acts::MultiEigenStepperLoop) an `IsMultiStepper`
0281 trait is true and the filter compiles in a multi-component path via
0282 `if constexpr`; the plain single-component filter therefore carries no runtime
0283 cost, and the extra per-actor state is elided entirely with
0284 `[[no_unique_address]]`. On each material surface the track state is convoluted
0285 with the Bethe–Heitler mixture through the shared
0286 `Acts::detail::Gsf::applyBetheHeitler`, the mixture is reduced (through the same
0287 `mixtureReducer` delegate, now also part of the CKF extensions) and merged back
0288 to a single representation before the measurement update — the GSF's surface
0289 algorithm of @ref gsf-algorithm, embedded in the combinatorial search.
0290
0291 The multi-component knobs (`maxComponents`, `weightCutoff`, `mergeMethod`,
0292 `betheHeitlerApprox`) live on @ref Acts::BremCombinatorialKalmanFilterOptions. The
0293 filter's `Options` alias resolves to that type *only* for a multi-stepper, so a
0294 single-component configuration cannot even name the multi-component parameters. In
0295 the examples, electron-hypothesis seeds are routed to a brem-enabled finder built
0296 over a `MultiStepperLoop`, using KL-distance reduction and the default
0297 Bethe–Heitler approximation, while all other seeds use the plain finder.
0298
0299 ## Implementation pointers {#gsf-implementation}
0300
0301 - Fitter and options: @ref Acts::GaussianSumFitter, @ref Acts::GsfOptions,
0302 @ref Acts::GsfComponent, @ref Acts::ComponentMergeMethod.
0303 - Bethe–Heitler approximation: @ref Acts::BetheHeitlerApprox,
0304 @ref Acts::PolynomialBetheHeitlerApprox,
0305 @ref Acts::makeDefaultBetheHeitlerApprox.
0306 - Mixture reduction: @ref Acts::reduceMixtureWithKLDistance,
0307 @ref Acts::reduceMixtureLargestWeights.
0308 - Multi-component transport: @ref Acts::MultiEigenStepperLoop,
0309 @ref Acts::MultiStepperSurfaceReached.
0310 - Bremsstrahlung recovery in finding: @ref Acts::CombinatorialKalmanFilter,
0311 @ref Acts::BremCombinatorialKalmanFilterOptions.
0312 - Shared material formalism (scattering / ionization): @ref kf_material_effects.
0313
0314 The per-surface algorithm itself lives in the internal `Acts::detail::Gsf`
0315 code; the snippets above are extracted directly from the corresponding headers.