Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-08-23 08:39:05

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 #pragma once
0010 
0011 #include "Acts/Definitions/Algebra.hpp"
0012 #include "Acts/Definitions/Common.hpp"
0013 #include "Acts/EventData/MeasurementHelpers.hpp"
0014 #include "Acts/EventData/MultiTrajectory.hpp"
0015 #include "Acts/EventData/SourceLink.hpp"
0016 #include "Acts/EventData/TrackProxyConcept.hpp"
0017 #include "Acts/EventData/Types.hpp"
0018 #include "Acts/EventData/VectorMultiTrajectory.hpp"
0019 #include "Acts/EventData/VectorTrackContainer.hpp"
0020 #include "Acts/EventData/detail/CorrectedTransformationFreeToBound.hpp"
0021 #include "Acts/Geometry/GeometryContext.hpp"
0022 #include "Acts/Geometry/TrackingVolume.hpp"
0023 #include "Acts/MagneticField/MagneticFieldContext.hpp"
0024 #include "Acts/Material/Interactions.hpp"
0025 #include "Acts/Propagator/DirectNavigator.hpp"
0026 #include "Acts/Propagator/PropagatorOptions.hpp"
0027 #include "Acts/Propagator/StandardAborters.hpp"
0028 #include "Acts/Propagator/detail/PointwiseMaterialInteraction.hpp"
0029 #include "Acts/TrackFitting/GlobalChiSquareFitterError.hpp"
0030 #include "Acts/TrackFitting/detail/VoidFitterComponents.hpp"
0031 #include "Acts/Utilities/CalibrationContext.hpp"
0032 #include "Acts/Utilities/Delegate.hpp"
0033 #include "Acts/Utilities/Logger.hpp"
0034 #include "Acts/Utilities/Result.hpp"
0035 #include "Acts/Utilities/TrackHelpers.hpp"
0036 
0037 #include <functional>
0038 #include <limits>
0039 #include <memory>
0040 #include <type_traits>
0041 #include <unordered_map>
0042 
0043 namespace Acts::Experimental {
0044 
0045 /// @addtogroup track_fitting
0046 /// @{
0047 
0048 namespace Gx2fConstants {
0049 constexpr std::string_view gx2fnUpdateColumn = "Gx2fnUpdateColumn";
0050 
0051 // Mask for the track states. We don't need Predicted and Filtered
0052 constexpr TrackStatePropMask trackStateMask = TrackStatePropMask::Smoothed |
0053                                               TrackStatePropMask::Jacobian |
0054                                               TrackStatePropMask::Calibrated;
0055 
0056 // A projector used for scattering. By using Jacobian * phiThetaProjector one
0057 // gets only the derivatives for the variables phi and theta.
0058 const Eigen::Matrix<double, eBoundSize, 2> phiThetaProjector = [] {
0059   Eigen::Matrix<double, eBoundSize, 2> m =
0060       Eigen::Matrix<double, eBoundSize, 2>::Zero();
0061   m(eBoundPhi, 0) = 1.0;
0062   m(eBoundTheta, 1) = 1.0;
0063   return m;
0064 }();
0065 }  // namespace Gx2fConstants
0066 
0067 /// Extension struct which holds delegates to customise the GX2F behaviour
0068 template <typename traj_t>
0069 struct Gx2FitterExtensions {
0070   /// Type alias for mutable track state proxy from multi-trajectory
0071   using TrackStateProxy = typename MultiTrajectory<traj_t>::TrackStateProxy;
0072   /// Type alias for const track state proxy from multi-trajectory
0073   using ConstTrackStateProxy =
0074       typename MultiTrajectory<traj_t>::ConstTrackStateProxy;
0075   /// Type alias for track parameters from track state proxy
0076   using Parameters = typename TrackStateProxy::Parameters;
0077 
0078   /// Type alias for calibrator delegate to process measurements
0079   using Calibrator =
0080       Delegate<void(const GeometryContext&, const CalibrationContext&,
0081                     const SourceLink&, TrackStateProxy)>;
0082 
0083   /// Type alias for updater delegate to incorporate measurements into track
0084   /// parameters
0085   using Updater = Delegate<Result<void>(const GeometryContext&, TrackStateProxy,
0086                                         const Logger&)>;
0087 
0088   /// Type alias for outlier finder delegate to identify measurement outliers
0089   using OutlierFinder = Delegate<bool(ConstTrackStateProxy)>;
0090 
0091   /// The Calibrator is a dedicated calibration algorithm that allows
0092   /// to calibrate measurements using track information, this could be
0093   /// e.g. sagging for wires, module deformations, etc.
0094   Calibrator calibrator;
0095 
0096   /// The updater incorporates measurement information into the track parameters
0097   Updater updater;
0098 
0099   /// Determines whether a measurement is supposed to be considered as an
0100   /// outlier
0101   OutlierFinder outlierFinder;
0102 
0103   /// Retrieves the associated surface from a source link
0104   SourceLinkSurfaceAccessor surfaceAccessor;
0105 
0106   /// Default constructor which connects the default void components
0107   Gx2FitterExtensions() {
0108     calibrator.template connect<&Acts::detail::voidFitterCalibrator<traj_t>>();
0109     updater.template connect<&Acts::detail::voidFitterUpdater<traj_t>>();
0110     outlierFinder.template connect<&Acts::detail::voidOutlierFinder<traj_t>>();
0111     surfaceAccessor.connect<&Acts::detail::voidSurfaceAccessor>();
0112   }
0113 };
0114 
0115 /// Combined options for the Global-Chi-Square fitter.
0116 ///
0117 /// @tparam traj_t The trajectory type
0118 template <typename traj_t>
0119 struct Gx2FitterOptions {
0120   /// PropagatorOptions with context.
0121   ///
0122   /// @param gctx The geometry context for this fit
0123   /// @param mctx The magnetic context for this fit
0124   /// @param cctx The calibration context for this fit
0125   /// @param extensions_ The KF extensions
0126   /// @param pOptions The plain propagator options
0127   /// @param rSurface The reference surface for the fit to be expressed at
0128   /// @param mScattering Whether to include multiple scattering
0129   /// @param eLoss Whether to include energy loss
0130   /// @param freeToBoundCorrection_ Correction for non-linearity effect during transform from free to bound
0131   /// @param nUpdateMax_ Max number of iterations for updating the parameters
0132   /// @param relChi2changeCutOff_ Check for convergence (abort condition). Set to 0 to skip.
0133   Gx2FitterOptions(const GeometryContext& gctx,
0134                    const MagneticFieldContext& mctx,
0135                    std::reference_wrapper<const CalibrationContext> cctx,
0136                    Gx2FitterExtensions<traj_t> extensions_,
0137                    const PropagatorPlainOptions& pOptions,
0138                    const Surface* rSurface = nullptr, bool mScattering = false,
0139                    bool eLoss = false,
0140                    const FreeToBoundCorrection& freeToBoundCorrection_ =
0141                        FreeToBoundCorrection(false),
0142                    const std::size_t nUpdateMax_ = 5,
0143                    double relChi2changeCutOff_ = 1e-5)
0144       : geoContext(gctx),
0145         magFieldContext(mctx),
0146         calibrationContext(cctx),
0147         extensions(extensions_),
0148         propagatorPlainOptions(pOptions),
0149         referenceSurface(rSurface),
0150         multipleScattering(mScattering),
0151         energyLoss(eLoss),
0152         freeToBoundCorrection(freeToBoundCorrection_),
0153         nUpdateMax(nUpdateMax_),
0154         relChi2changeCutOff(relChi2changeCutOff_) {}
0155 
0156   /// Contexts are required and the options must not be default-constructible.
0157   Gx2FitterOptions() = delete;
0158 
0159   /// Context object for the geometry
0160   std::reference_wrapper<const GeometryContext> geoContext;
0161   /// Context object for the magnetic field
0162   std::reference_wrapper<const MagneticFieldContext> magFieldContext;
0163   /// context object for the calibration
0164   std::reference_wrapper<const CalibrationContext> calibrationContext;
0165 
0166   /// Extensions for calibration and outlier finding
0167   Gx2FitterExtensions<traj_t> extensions;
0168 
0169   /// The trivial propagator options
0170   PropagatorPlainOptions propagatorPlainOptions;
0171 
0172   /// The reference Surface
0173   const Surface* referenceSurface = nullptr;
0174 
0175   /// Whether to consider multiple scattering
0176   bool multipleScattering = false;
0177 
0178   /// Whether to consider energy loss
0179   bool energyLoss = false;
0180 
0181   /// Whether to include non-linear correction during global to local
0182   /// transformation
0183   FreeToBoundCorrection freeToBoundCorrection;
0184 
0185   /// Max number of iterations during the fit (abort condition)
0186   std::size_t nUpdateMax = 5;
0187 
0188   /// Check for convergence (abort condition). Set to 0 to skip.
0189   double relChi2changeCutOff = 1e-7;
0190 };
0191 
0192 /// Result container for a global chi-square fit.
0193 template <typename traj_t>
0194 struct Gx2FitterResult {
0195   /// Fitted states that the actor has handled.
0196   traj_t* fittedStates{nullptr};
0197 
0198   /// This is the index of the 'tip' of the track stored in multitrajectory.
0199   /// This corresponds to the last measurement state in the multitrajectory.
0200   /// Since this KF only stores one trajectory, it is unambiguous.
0201   /// Acts::TrackTraits::kInvalid is the start of a trajectory.
0202   std::size_t lastMeasurementIndex = Acts::kTrackIndexInvalid;
0203 
0204   /// This is the index of the 'tip' of the states stored in multitrajectory.
0205   /// This corresponds to the last state in the multitrajectory.
0206   /// Since this KF only stores one trajectory, it is unambiguous.
0207   /// Acts::TrackTraits::kInvalid is the start of a trajectory.
0208   std::size_t lastTrackIndex = Acts::kTrackIndexInvalid;
0209 
0210   /// The optional Parameters at the provided surface
0211   std::optional<BoundTrackParameters> fittedParameters;
0212 
0213   /// Counter for states with non-outlier measurements
0214   std::size_t measurementStates = 0;
0215 
0216   /// Counter for measurements holes
0217   /// A hole correspond to a surface with an associated detector element with no
0218   /// associated measurement. Holes are only taken into account if they are
0219   /// between the first and last measurements.
0220   std::size_t measurementHoles = 0;
0221 
0222   /// Counter for handled states
0223   std::size_t processedStates = 0;
0224 
0225   /// Counter for handled measurements
0226   std::size_t processedMeasurements = 0;
0227 
0228   /// Indicator if track fitting has been done
0229   bool finished = false;
0230 
0231   /// Measurement surfaces without hits
0232   std::vector<const Surface*> missedActiveSurfaces;
0233 
0234   /// Measurement surfaces handled in both forward and
0235   /// backward filtering
0236   std::vector<const Surface*> passedAgainSurfaces;
0237 
0238   /// Count how many surfaces have been hit
0239   std::size_t surfaceCount = 0;
0240 };
0241 
0242 /// @brief A container to store scattering properties for each material surface
0243 ///
0244 /// This struct holds the scattering angles, the inverse covariance of the
0245 /// material, and a validity flag indicating whether the material is valid for
0246 /// the scattering process.
0247 struct ScatteringProperties {
0248  public:
0249   /// @brief Constructor to initialize scattering properties.
0250   ///
0251   /// @param scatteringAngles_ The vector of scattering angles.
0252   /// @param invCovarianceMaterial_ The inverse covariance of the material.
0253   /// @param materialIsValid_ A boolean flag indicating whether the material is valid.
0254   ScatteringProperties(const BoundVector& scatteringAngles_,
0255                        const double invCovarianceMaterial_,
0256                        const bool materialIsValid_)
0257       : m_scatteringAngles(scatteringAngles_),
0258         m_invCovarianceMaterial(invCovarianceMaterial_),
0259         m_materialIsValid(materialIsValid_) {}
0260 
0261   /// @brief Accessor for the scattering angles (const version)
0262   /// @return Const reference to the vector of scattering angles
0263   const BoundVector& scatteringAngles() const { return m_scatteringAngles; }
0264 
0265   /// @brief Accessor for the scattering angles (mutable version)
0266   /// @return Mutable reference to the vector of scattering angles for modification
0267   BoundVector& scatteringAngles() { return m_scatteringAngles; }
0268 
0269   /// @brief Accessor for the inverse covariance of the material
0270   /// @return Inverse covariance value computed from material properties (e.g., Highland formula)
0271   double invCovarianceMaterial() const { return m_invCovarianceMaterial; }
0272 
0273   /// @brief Accessor for the material validity flag
0274   /// @return True if material is valid for scattering calculations, false for vacuum or zero thickness
0275   bool materialIsValid() const { return m_materialIsValid; }
0276 
0277  private:
0278   /// Vector of scattering angles. The vector is usually all zeros except for
0279   /// eBoundPhi and eBoundTheta.
0280   BoundVector m_scatteringAngles;
0281 
0282   /// Inverse covariance of the material. Compute with e.g. the Highland
0283   /// formula.
0284   double m_invCovarianceMaterial;
0285 
0286   /// Flag indicating whether the material is valid. Commonly vacuum and zero
0287   /// thickness material will be ignored.
0288   bool m_materialIsValid;
0289 };
0290 
0291 /// @brief A container to manage all properties of a gx2f system
0292 ///
0293 /// This struct manages the mathematical infrastructure for the gx2f. It
0294 /// initializes and maintains the extended aMatrix and extended bVector.
0295 struct Gx2fSystem {
0296  public:
0297   /// @brief Constructor to initialize matrices and vectors to zero based on specified dimensions.
0298   ///
0299   /// @param nDims Number of dimensions for the extended matrix and vector.
0300   explicit Gx2fSystem(std::size_t nDims)
0301       : m_nDims{nDims},
0302         m_aMatrix{Eigen::MatrixXd::Zero(nDims, nDims)},
0303         m_bVector{Eigen::VectorXd::Zero(nDims)} {}
0304 
0305   /// @brief Accessor for the number of dimensions of the extended system
0306   /// @return Number of dimensions for the aMatrix and bVector (bound parameters + scattering angles)
0307   std::size_t nDims() const { return m_nDims; }
0308 
0309   /// @brief Accessor for the accumulated chi-squared value (const version)
0310   /// @return Current sum of chi-squared contributions from measurements and material
0311   double chi2() const { return m_chi2; }
0312 
0313   /// @brief Accessor for the accumulated chi-squared value (mutable version)
0314   /// @return Mutable reference to chi-squared sum for modification during fitting
0315   double& chi2() { return m_chi2; }
0316 
0317   /// @brief Accessor for the extended system matrix (const version)
0318   /// @return Const reference to the aMatrix containing measurement and material contributions
0319   const Eigen::MatrixXd& aMatrix() const { return m_aMatrix; }
0320 
0321   /// @brief Accessor for the extended system matrix (mutable version)
0322   /// @return Mutable reference to the aMatrix for adding measurement and material contributions
0323   Eigen::MatrixXd& aMatrix() { return m_aMatrix; }
0324 
0325   /// @brief Accessor for the extended system vector (const version)
0326   /// @return Const reference to the bVector containing measurement and material contributions
0327   const Eigen::VectorXd& bVector() const { return m_bVector; }
0328 
0329   /// @brief Accessor for the extended system vector (mutable version)
0330   /// @return Mutable reference to the bVector for adding measurement and material contributions
0331   Eigen::VectorXd& bVector() { return m_bVector; }
0332 
0333   /// @brief Accessor for the number of degrees of freedom (const version)
0334   /// @return Current number of degrees of freedom from processed measurements
0335   std::size_t ndf() const { return m_ndf; }
0336 
0337   /// @brief Accessor for the number of degrees of freedom (mutable version)
0338   /// @return Mutable reference to NDF counter for incrementing during measurement processing
0339   std::size_t& ndf() { return m_ndf; }
0340 
0341   /// @brief Determines the minimum number of degrees of freedom required for the fit
0342   ///
0343   /// Automatically deduces the required NDF based on the system configuration.
0344   /// We have only 3 cases, because we always have l0, l1, phi, theta:
0345   /// - 4: no magnetic field -> q/p is empty
0346   /// - 5: no time measurement -> time is not fittable
0347   /// - 6: full fit with all parameters
0348   ///
0349   /// @return Required NDF based on which parameters can be fitted
0350   std::size_t findRequiredNdf() {
0351     std::size_t ndfSystem = 0;
0352     if (m_aMatrix(4, 4) == 0) {
0353       ndfSystem = 4;
0354     } else if (m_aMatrix(5, 5) == 0) {
0355       ndfSystem = 5;
0356     } else {
0357       ndfSystem = 6;
0358     }
0359 
0360     return ndfSystem;
0361   }
0362 
0363   /// @brief Checks if the system has sufficient degrees of freedom for fitting
0364   /// @return True if NDF exceeds the minimum required for the parameter configuration
0365   bool isWellDefined() { return m_ndf > findRequiredNdf(); }
0366 
0367  private:
0368   /// Number of dimensions of the (extended) system
0369   std::size_t m_nDims;
0370 
0371   /// Sum of chi-squared values.
0372   double m_chi2 = 0.;
0373 
0374   /// Extended matrix for accumulation.
0375   Eigen::MatrixXd m_aMatrix;
0376 
0377   /// Extended vector for accumulation.
0378   Eigen::VectorXd m_bVector;
0379 
0380   /// Number of degrees of freedom of the system
0381   std::size_t m_ndf = 0u;
0382 };
0383 
0384 /// @brief Adds a measurement to the GX2F equation system in a modular backend function.
0385 ///
0386 /// This function processes measurement data and integrates it into the GX2F
0387 /// system.
0388 ///
0389 /// @param extendedSystem All parameters of the current equation system to update.
0390 /// @param jacobianFromStart The Jacobian matrix from the start to the current state.
0391 /// @param covarianceMeasurement The covariance matrix of the measurement.
0392 /// @param predicted The predicted state vector based on the track state.
0393 /// @param measurement The measurement vector.
0394 /// @param projector The projection matrix.
0395 /// @param logger A logger instance.
0396 ///
0397 /// @note The dynamic Eigen matrices are suboptimal. We could think of
0398 /// templating again in the future on kMeasDims. We currently use dynamic
0399 /// matrices to reduce the memory during compile time.
0400 void addMeasurementToGx2fSumsBackend(
0401     Gx2fSystem& extendedSystem,
0402     const std::vector<BoundMatrix>& jacobianFromStart,
0403     const Eigen::MatrixXd& covarianceMeasurement, const BoundVector& predicted,
0404     const Eigen::VectorXd& measurement, const Eigen::MatrixXd& projector,
0405     const Logger& logger);
0406 
0407 /// @brief Process measurements and fill the aMatrix and bVector
0408 ///
0409 /// The function processes each measurement for the GX2F Actor fitting process.
0410 /// It extracts the information from the track state and adds it to aMatrix,
0411 /// bVector, and chi2sum.
0412 ///
0413 /// @tparam kMeasDim Number of dimensions of the measurement
0414 /// @tparam track_state_t The type of the track state
0415 ///
0416 /// @param extendedSystem All parameters of the current equation system to update
0417 /// @param jacobianFromStart The Jacobian matrix from start to the current state
0418 /// @param trackState The track state to analyse
0419 /// @param logger A logger instance
0420 template <std::size_t kMeasDim, typename track_state_t>
0421 void addMeasurementToGx2fSums(Gx2fSystem& extendedSystem,
0422                               const std::vector<BoundMatrix>& jacobianFromStart,
0423                               const track_state_t& trackState,
0424                               const Logger& logger) {
0425   const SquareMatrix<kMeasDim> covarianceMeasurement =
0426       trackState.template calibratedCovariance<kMeasDim>();
0427 
0428   const BoundVector predicted = trackState.smoothed();
0429 
0430   const Vector<kMeasDim> measurement =
0431       trackState.template calibrated<kMeasDim>();
0432 
0433   const Matrix<kMeasDim, eBoundSize> projector =
0434       trackState.template projectorSubspaceHelper<kMeasDim>().projector();
0435 
0436   addMeasurementToGx2fSumsBackend(extendedSystem, jacobianFromStart,
0437                                   covarianceMeasurement, predicted, measurement,
0438                                   projector, logger);
0439 }
0440 
0441 /// @brief Process material and fill the aMatrix and bVector
0442 ///
0443 /// The function processes each material for the GX2F Actor fitting process.
0444 /// It extracts the information from the track state and adds it to aMatrix,
0445 /// bVector, and chi2sum.
0446 ///
0447 /// @tparam track_state_t The type of the track state
0448 ///
0449 /// @param extendedSystem All parameters of the current equation system
0450 /// @param nMaterialsHandled How many materials we already handled. Used for the offset.
0451 /// @param scatteringMap The scattering map, containing all scattering angles and covariances
0452 /// @param trackState The track state to analyse
0453 /// @param logger A logger instance
0454 template <typename track_state_t>
0455 void addMaterialToGx2fSums(
0456     Gx2fSystem& extendedSystem, const std::size_t nMaterialsHandled,
0457     const std::unordered_map<GeometryIdentifier, ScatteringProperties>&
0458         scatteringMap,
0459     const track_state_t& trackState, const Logger& logger) {
0460   // Get and store geoId for the current material surface
0461   const GeometryIdentifier geoId = trackState.referenceSurface().geometryId();
0462   const auto scatteringMapId = scatteringMap.find(geoId);
0463   if (scatteringMapId == scatteringMap.end()) {
0464     ACTS_ERROR("No scattering angles found for material surface " << geoId);
0465     throw std::runtime_error(
0466         "No scattering angles found for material surface.");
0467   }
0468 
0469   const double sinThetaLoc = std::sin(trackState.smoothed()[eBoundTheta]);
0470 
0471   // The position, where we need to insert the values in aMatrix and bVector
0472   const std::size_t deltaPosition = eBoundSize + 2 * nMaterialsHandled;
0473 
0474   const BoundVector& scatteringAngles =
0475       scatteringMapId->second.scatteringAngles();
0476 
0477   const double invCov = scatteringMapId->second.invCovarianceMaterial();
0478 
0479   // Phi contribution
0480   extendedSystem.aMatrix()(deltaPosition, deltaPosition) +=
0481       invCov * sinThetaLoc * sinThetaLoc;
0482   extendedSystem.bVector()(deltaPosition, 0) -=
0483       invCov * scatteringAngles[eBoundPhi] * sinThetaLoc;
0484   extendedSystem.chi2() += invCov * scatteringAngles[eBoundPhi] * sinThetaLoc *
0485                            scatteringAngles[eBoundPhi] * sinThetaLoc;
0486 
0487   // Theta Contribution
0488   extendedSystem.aMatrix()(deltaPosition + 1, deltaPosition + 1) += invCov;
0489   extendedSystem.bVector()(deltaPosition + 1, 0) -=
0490       invCov * scatteringAngles[eBoundTheta];
0491   extendedSystem.chi2() +=
0492       invCov * scatteringAngles[eBoundTheta] * scatteringAngles[eBoundTheta];
0493 
0494   ACTS_VERBOSE(
0495       "Contributions in addMaterialToGx2fSums:\n"
0496       << "    invCov:        " << invCov << "\n"
0497       << "    sinThetaLoc:   " << sinThetaLoc << "\n"
0498       << "    deltaPosition: " << deltaPosition << "\n"
0499       << "    Phi:\n"
0500       << "        scattering angle:     " << scatteringAngles[eBoundPhi] << "\n"
0501       << "        aMatrix contribution: " << invCov * sinThetaLoc * sinThetaLoc
0502       << "\n"
0503       << "        bVector contribution: "
0504       << invCov * scatteringAngles[eBoundPhi] * sinThetaLoc << "\n"
0505       << "        chi2sum contribution: "
0506       << invCov * scatteringAngles[eBoundPhi] * sinThetaLoc *
0507              scatteringAngles[eBoundPhi] * sinThetaLoc
0508       << "\n"
0509       << "    Theta:\n"
0510       << "        scattering angle:     " << scatteringAngles[eBoundTheta]
0511       << "\n"
0512       << "        aMatrix contribution: " << invCov << "\n"
0513       << "        bVector contribution: "
0514       << invCov * scatteringAngles[eBoundTheta] << "\n"
0515       << "        chi2sum contribution: "
0516       << invCov * scatteringAngles[eBoundTheta] * scatteringAngles[eBoundTheta]
0517       << "\n");
0518 
0519   return;
0520 }
0521 
0522 /// @brief Fill the GX2F system with data from a track
0523 ///
0524 /// This function processes a track proxy and updates the aMatrix, bVector, and
0525 /// chi2 values for the GX2F fitting system. It considers material only if
0526 /// multiple scattering is enabled.
0527 ///
0528 /// @tparam track_proxy_t The type of the track proxy
0529 ///
0530 /// @param track A constant track proxy to inspect
0531 /// @param extendedSystem All parameters of the current equation system
0532 /// @param multipleScattering Flag to consider multiple scattering in the calculation
0533 /// @param scatteringMap Map of geometry identifiers to scattering properties,
0534 ///        containing scattering angles and validation status
0535 /// @param geoIdVector A vector to store geometry identifiers for tracking processed elements
0536 /// @param logger A logger instance
0537 template <TrackProxyConcept track_proxy_t>
0538 void fillGx2fSystem(
0539     const track_proxy_t track, Gx2fSystem& extendedSystem,
0540     const bool multipleScattering,
0541     const std::unordered_map<GeometryIdentifier, ScatteringProperties>&
0542         scatteringMap,
0543     std::vector<GeometryIdentifier>& geoIdVector, const Logger& logger) {
0544   std::vector<BoundMatrix> jacobianFromStart;
0545   jacobianFromStart.emplace_back(BoundMatrix::Identity());
0546 
0547   for (const auto& trackState : track.trackStates()) {
0548     // Get and store geoId for the current surface
0549     const GeometryIdentifier geoId = trackState.referenceSurface().geometryId();
0550     ACTS_DEBUG("Start to investigate trackState on surface " << geoId);
0551     const auto typeFlags = trackState.typeFlags();
0552     const bool stateHasMeasurement = typeFlags.hasMeasurement();
0553     const bool stateHasMaterial = typeFlags.hasMaterial();
0554 
0555     // First we figure out, if we would need to look into material
0556     // surfaces at all. Later, we also check, if the material slab is
0557     // valid, otherwise we modify this flag to ignore the material
0558     // completely.
0559     bool doMaterial = multipleScattering && stateHasMaterial;
0560     if (doMaterial) {
0561       const auto scatteringMapId = scatteringMap.find(geoId);
0562       assert(scatteringMapId != scatteringMap.end() &&
0563              "No scattering angles found for material surface.");
0564       doMaterial = doMaterial && scatteringMapId->second.materialIsValid();
0565     }
0566 
0567     // We only consider states with a measurement (and/or material)
0568     if (!stateHasMeasurement && !doMaterial) {
0569       ACTS_DEBUG("    Skip state.");
0570       continue;
0571     }
0572 
0573     // update all Jacobians from start
0574     for (auto& jac : jacobianFromStart) {
0575       jac = trackState.jacobian() * jac;
0576     }
0577 
0578     // Handle measurement
0579     if (stateHasMeasurement) {
0580       ACTS_DEBUG("    Handle measurement.");
0581 
0582       const auto measDim = trackState.calibratedSize();
0583 
0584       if (measDim < 1 || 6 < measDim) {
0585         ACTS_ERROR("Can not process state with measurement with "
0586                    << measDim << " dimensions.");
0587         throw std::domain_error(
0588             "Found measurement with less than 1 or more than 6 dimension(s).");
0589       }
0590 
0591       extendedSystem.ndf() += measDim;
0592 
0593       visit_measurement(measDim, [&](auto N) {
0594         addMeasurementToGx2fSums<N>(extendedSystem, jacobianFromStart,
0595                                     trackState, logger);
0596       });
0597     }
0598 
0599     // Handle material
0600     if (doMaterial) {
0601       ACTS_DEBUG("    Handle material");
0602       // Add for this material a new Jacobian, starting from this surface.
0603       jacobianFromStart.emplace_back(BoundMatrix::Identity());
0604 
0605       // Add the material contribution to the system
0606       addMaterialToGx2fSums(extendedSystem, geoIdVector.size(), scatteringMap,
0607                             trackState, logger);
0608 
0609       geoIdVector.emplace_back(geoId);
0610     }
0611   }
0612 }
0613 
0614 /// @brief Count the valid material states in a track for scattering calculations.
0615 ///
0616 /// This function counts the valid material surfaces encountered in a track
0617 /// by examining each track state. The count is based on the presence of
0618 /// material flags and the availability of scattering information for each
0619 /// surface.
0620 ///
0621 /// @tparam track_proxy_t The type of the track proxy
0622 ///
0623 /// @param track A constant track proxy to inspect
0624 /// @param scatteringMap Map of geometry identifiers to scattering properties,
0625 ///        containing scattering angles and validation status
0626 /// @param logger A logger instance
0627 /// @return Number of valid material states in the track
0628 template <TrackProxyConcept track_proxy_t>
0629 std::size_t countMaterialStates(
0630     const track_proxy_t track,
0631     const std::unordered_map<GeometryIdentifier, ScatteringProperties>&
0632         scatteringMap,
0633     const Logger& logger) {
0634   std::size_t nMaterialSurfaces = 0;
0635   ACTS_DEBUG("Count the valid material surfaces.");
0636   for (const auto& trackState : track.trackStates()) {
0637     const auto typeFlags = trackState.typeFlags();
0638     const bool stateHasMaterial = typeFlags.hasMaterial();
0639 
0640     if (!stateHasMaterial) {
0641       continue;
0642     }
0643 
0644     // Get and store geoId for the current material surface
0645     const GeometryIdentifier geoId = trackState.referenceSurface().geometryId();
0646 
0647     const auto scatteringMapId = scatteringMap.find(geoId);
0648     assert(scatteringMapId != scatteringMap.end() &&
0649            "No scattering angles found for material surface.");
0650     if (!scatteringMapId->second.materialIsValid()) {
0651       continue;
0652     }
0653 
0654     nMaterialSurfaces++;
0655   }
0656 
0657   return nMaterialSurfaces;
0658 }
0659 
0660 /// @brief Solve the gx2f system to get the delta parameters for the update
0661 ///
0662 /// This function computes the delta parameters for the GX2F Actor fitting
0663 /// process by solving the linear equation system [a] * delta = b. It uses the
0664 /// column-pivoting Householder QR decomposition for numerical stability.
0665 ///
0666 /// @param extendedSystem All parameters of the current equation system
0667 /// @return Delta parameters for the GX2F update
0668 Eigen::VectorXd computeGx2fDeltaParams(const Gx2fSystem& extendedSystem);
0669 
0670 /// @brief Update parameters (and scattering angles if applicable)
0671 ///
0672 /// @param params Parameters to be updated
0673 /// @param deltaParamsExtended Delta parameters for bound parameter and scattering angles
0674 /// @param nMaterialSurfaces Number of material surfaces in the track
0675 /// @param scatteringMap Map of geometry identifiers to scattering properties,
0676 ///        containing all scattering angles and covariances
0677 /// @param geoIdVector Vector of geometry identifiers corresponding to material surfaces
0678 void updateGx2fParams(
0679     BoundTrackParameters& params, const Eigen::VectorXd& deltaParamsExtended,
0680     const std::size_t nMaterialSurfaces,
0681     std::unordered_map<GeometryIdentifier, ScatteringProperties>& scatteringMap,
0682     const std::vector<GeometryIdentifier>& geoIdVector);
0683 
0684 /// @brief Calculate and update the covariance of the fitted parameters
0685 ///
0686 /// This function calculates the covariance of the fitted parameters using
0687 /// cov = inv([a])
0688 /// It then updates the first square block of size ndfSystem. This ensures,
0689 /// that we only update the covariance for fitted parameters. (In case of
0690 /// no qop/time fit)
0691 ///
0692 /// @param fullCovariancePredicted The covariance matrix to update
0693 /// @param extendedSystem All parameters of the current equation system
0694 void updateGx2fCovarianceParams(BoundMatrix& fullCovariancePredicted,
0695                                 Gx2fSystem& extendedSystem);
0696 
0697 /// Global Chi Square fitter (GX2F) implementation.
0698 ///
0699 /// @tparam propagator_t Type of the propagation class
0700 ///
0701 /// TODO Write description
0702 template <typename propagator_t, typename traj_t>
0703 class Gx2Fitter {
0704   /// The navigator type
0705   using Gx2fNavigator = typename propagator_t::Navigator;
0706 
0707   /// The navigator has DirectNavigator type or not
0708   static constexpr bool isDirectNavigator =
0709       std::is_same_v<Gx2fNavigator, DirectNavigator>;
0710 
0711   static constexpr auto kInvalid = kTrackIndexInvalid;
0712 
0713  public:
0714   /// @brief Constructor for the Global Chi-Square Fitter
0715   ///
0716   /// Initializes the fitter with a propagator and optional logger.
0717   /// The fitter uses iterative fitting with a linear equation system
0718   /// to minimize chi-squared including multiple scattering effects.
0719   ///
0720   /// @param pPropagator The propagator instance for track propagation
0721   /// @param _logger Logger instance for debugging output (optional)
0722   explicit Gx2Fitter(const propagator_t& pPropagator,
0723                      std::unique_ptr<const Logger> _logger =
0724                          getDefaultLogger("Gx2Fitter", Logging::INFO))
0725       : m_propagator(pPropagator),
0726         m_logger{std::move(_logger)},
0727         m_actorLogger{m_logger->cloneWithSuffix("Actor")},
0728         m_addToSumLogger{m_logger->cloneWithSuffix("AddToSum")} {}
0729 
0730  private:
0731   /// The propagator for the transport and material update
0732   propagator_t m_propagator;
0733 
0734   /// The logger instance
0735   std::unique_ptr<const Logger> m_logger;
0736   std::unique_ptr<const Logger> m_actorLogger;
0737   std::unique_ptr<const Logger> m_addToSumLogger;
0738 
0739   const Logger& logger() const { return *m_logger; }
0740 
0741   /// @brief Propagator Actor plugin for the GX2F
0742   ///
0743   /// @tparam parameters_t The type of parameters used for "local" parameters.
0744   /// @tparam calibrator_t The type of calibrator
0745   /// @tparam outlier_finder_t Type of the outlier finder class
0746   ///
0747   /// The GX2F Actor does not rely on the measurements to be sorted along the
0748   /// track.
0749   class Actor {
0750    public:
0751     /// Broadcast the result_type
0752     using result_type = Gx2FitterResult<traj_t>;
0753 
0754     /// The target surface
0755     const Surface* targetSurface = nullptr;
0756 
0757     /// Allows retrieving measurements for a surface
0758     const std::unordered_map<const Surface*, SourceLink>* inputMeasurements{};
0759 
0760     /// Whether to consider multiple scattering.
0761     bool multipleScattering = false;
0762 
0763     /// Whether to consider energy loss.
0764     bool energyLoss = false;  /// TODO implement later
0765 
0766     /// Whether to include non-linear correction during global to local
0767     /// transformation
0768     FreeToBoundCorrection freeToBoundCorrection;
0769 
0770     /// Input MultiTrajectory
0771     std::shared_ptr<MultiTrajectory<traj_t>> outputStates;
0772 
0773     /// The logger instance
0774     const Logger* actorLogger{nullptr};
0775 
0776     /// Logger helper
0777     const Logger& logger() const { return *actorLogger; }
0778 
0779     Gx2FitterExtensions<traj_t> extensions;
0780 
0781     /// The Surface being
0782     SurfaceReached targetReached;
0783 
0784     /// Calibration context for the fit
0785     const CalibrationContext* calibrationContext{nullptr};
0786 
0787     /// The particle hypothesis is needed for estimating scattering angles
0788     const BoundTrackParameters* parametersWithHypothesis = nullptr;
0789 
0790     /// The scatteringMap stores for each visited surface their scattering
0791     /// properties
0792     std::unordered_map<GeometryIdentifier, ScatteringProperties>*
0793         scatteringMap = nullptr;
0794 
0795     /// @brief Gx2f actor operation
0796     ///
0797     /// @tparam propagator_state_t is the type of Propagator state
0798     /// @tparam stepper_t Type of the stepper
0799     /// @tparam navigator_t Type of the navigator
0800     ///
0801     /// @param state is the mutable propagator state object
0802     /// @param stepper The stepper in use
0803     /// @param navigator The navigator in use
0804     /// @param result is the mutable result state object
0805     template <typename propagator_state_t, typename stepper_t,
0806               typename navigator_t>
0807     Result<void> act(propagator_state_t& state, const stepper_t& stepper,
0808                      const navigator_t& navigator, result_type& result,
0809                      const Logger& /*logger*/) const {
0810       assert(result.fittedStates && "No MultiTrajectory set");
0811 
0812       // Check if we can stop to propagate
0813       if (result.measurementStates == inputMeasurements->size()) {
0814         ACTS_DEBUG("Actor: finish: All measurements have been found.");
0815         result.finished = true;
0816       } else if (state.navigation.navigationBreak) {
0817         ACTS_DEBUG("Actor: finish: navigationBreak.");
0818         result.finished = true;
0819       }
0820 
0821       // End the propagation and return to the fitter
0822       if (result.finished) {
0823         // Remove the missing surfaces that occur after the last measurement
0824         if (result.measurementStates > 0) {
0825           result.missedActiveSurfaces.resize(result.measurementHoles);
0826         }
0827 
0828         return Result<void>::success();
0829       }
0830 
0831       // We are only interested in surfaces. If we are not on a surface, we
0832       // continue the navigation
0833       auto surface = navigator.currentSurface(state.navigation);
0834       if (surface == nullptr) {
0835         return Result<void>::success();
0836       }
0837 
0838       ++result.surfaceCount;
0839       const GeometryIdentifier geoId = surface->geometryId();
0840       ACTS_DEBUG("Surface " << geoId << " detected.");
0841 
0842       const bool surfaceIsSensitive = surface->isSensitive();
0843       const bool surfaceHasMaterial = surface->hasMaterial();
0844       // First we figure out, if we would need to look into material surfaces at
0845       // all. Later, we also check, if the material slab is valid, otherwise we
0846       // modify this flag to ignore the material completely.
0847       bool doMaterial = multipleScattering && surfaceHasMaterial;
0848 
0849       // Found material - add a scatteringAngles entry if not done yet.
0850       // Handling will happen later
0851       if (doMaterial) {
0852         ACTS_DEBUG("    The surface contains material, ...");
0853 
0854         auto scatteringMapId = scatteringMap->find(geoId);
0855         if (scatteringMapId == scatteringMap->end()) {
0856           ACTS_DEBUG("    ... create entry in scattering map.");
0857 
0858           const Result<MaterialSlab> slabResult =
0859               Acts::detail::evaluateMaterialSlab(
0860                   state, stepper, *surface,
0861                   Acts::detail::determineMaterialUpdateMode(
0862                       state, navigator, MaterialUpdateMode::FullUpdate));
0863           if (!slabResult.ok()) {
0864             ACTS_DEBUG("GlobalChiSquareFitter | "
0865                        << "Failed to evaluate material slab: "
0866                        << slabResult.error());
0867             return Result<void>::failure(slabResult.error());
0868           }
0869           const MaterialSlab& slab = *slabResult;
0870           const bool slabIsValid = !slab.isVacuum();
0871 
0872           double invSigma2 = 0.;
0873           if (slabIsValid) {
0874             const auto& particle =
0875                 parametersWithHypothesis->particleHypothesis();
0876 
0877             const double sigma =
0878                 static_cast<double>(Acts::computeMultipleScatteringTheta0(
0879                     slab, particle.absolutePdg(), particle.mass(),
0880                     static_cast<float>(
0881                         parametersWithHypothesis->parameters()[eBoundQOverP]),
0882                     particle.absoluteCharge()));
0883             ACTS_VERBOSE(
0884                 "        The Highland formula gives sigma = " << sigma);
0885             invSigma2 = 1. / std::pow(sigma, 2);
0886           } else {
0887             ACTS_VERBOSE("        Material slab is not valid.");
0888           }
0889 
0890           scatteringMap->emplace(
0891               geoId, ScatteringProperties{BoundVector::Zero(), invSigma2,
0892                                           slabIsValid});
0893           scatteringMapId = scatteringMap->find(geoId);
0894         } else {
0895           ACTS_DEBUG("    ... found entry in scattering map.");
0896         }
0897 
0898         doMaterial = doMaterial && scatteringMapId->second.materialIsValid();
0899       }
0900 
0901       // Here we handle all measurements
0902       if (auto sourceLinkIt = inputMeasurements->find(surface);
0903           sourceLinkIt != inputMeasurements->end()) {
0904         ACTS_DEBUG("    The surface contains a measurement.");
0905 
0906         // Transport the covariance to the surface
0907         stepper.transportCovarianceToBound(state.stepping, *surface,
0908                                            freeToBoundCorrection);
0909 
0910         // TODO generalize the update of the currentTrackIndex
0911         auto& fittedStates = *result.fittedStates;
0912 
0913         // Add a <trackStateMask> TrackState entry multi trajectory. This
0914         // allocates storage for all components, which we will set later.
0915         typename traj_t::TrackStateProxy trackStateProxy =
0916             fittedStates.makeTrackState(Gx2fConstants::trackStateMask,
0917                                         result.lastTrackIndex);
0918         const std::size_t currentTrackIndex = trackStateProxy.index();
0919 
0920         // Set the trackStateProxy components with the state from the ongoing
0921         // propagation
0922         {
0923           trackStateProxy.setReferenceSurface(surface->getSharedPtr());
0924           // Bind the transported state to the current surface
0925           auto res = stepper.boundState(state.stepping, *surface, false,
0926                                         freeToBoundCorrection);
0927           if (!res.ok()) {
0928             return res.error();
0929           }
0930           // Not const since, we might need to update with scattering angles
0931           auto& [boundParams, jacobian, pathLength] = *res;
0932 
0933           // For material surfaces, we also update the angles with the
0934           // available scattering information
0935           if (doMaterial) {
0936             ACTS_DEBUG("    Update parameters with scattering angles.");
0937             const auto scatteringMapId = scatteringMap->find(geoId);
0938             ACTS_VERBOSE(
0939                 "        scatteringAngles: "
0940                 << scatteringMapId->second.scatteringAngles().transpose());
0941             ACTS_VERBOSE("        boundParams before the update: "
0942                          << boundParams.parameters().transpose());
0943             boundParams.parameters() +=
0944                 scatteringMapId->second.scatteringAngles();
0945             ACTS_VERBOSE("        boundParams after the update: "
0946                          << boundParams.parameters().transpose());
0947           }
0948 
0949           // Fill the track state
0950           trackStateProxy.smoothed() = boundParams.parameters();
0951           trackStateProxy.smoothedCovariance() = state.stepping.cov;
0952 
0953           trackStateProxy.jacobian() = jacobian;
0954           trackStateProxy.pathLength() = pathLength;
0955 
0956           if (doMaterial) {
0957             stepper.update(state.stepping,
0958                            transformBoundToFreeParameters(
0959                                trackStateProxy.referenceSurface(),
0960                                state.geoContext, trackStateProxy.smoothed()),
0961                            trackStateProxy.smoothed(),
0962                            trackStateProxy.smoothedCovariance(), *surface);
0963           }
0964         }
0965 
0966         // We have smoothed parameters, so calibrate the uncalibrated input
0967         // measurement
0968         extensions.calibrator(state.geoContext, *calibrationContext,
0969                               sourceLinkIt->second, trackStateProxy);
0970 
0971         // Get and set the type flags
0972         auto typeFlags = trackStateProxy.typeFlags();
0973         typeFlags.setHasParameters();
0974         if (surfaceHasMaterial) {
0975           typeFlags.setHasMaterial();
0976         }
0977 
0978         // Set the measurement type flag
0979         typeFlags.setIsMeasurement();
0980         // We count the processed measurement
0981         ++result.processedMeasurements;
0982 
0983         result.lastMeasurementIndex = currentTrackIndex;
0984         result.lastTrackIndex = currentTrackIndex;
0985 
0986         // TODO check for outlier first
0987         // We count the state with measurement
0988         ++result.measurementStates;
0989 
0990         // We count the processed state
0991         ++result.processedStates;
0992 
0993         // Update the number of holes count only when encountering a
0994         // measurement
0995         result.measurementHoles = result.missedActiveSurfaces.size();
0996 
0997         return Result<void>::success();
0998       }
0999 
1000       if (doMaterial) {
1001         // Here we handle material for multipleScattering. If holes exist, we
1002         // also handle them already. We create a full trackstate (unlike for
1003         // simple holes), since we need to evaluate the material later
1004         ACTS_DEBUG(
1005             "    The surface contains no measurement, but material and maybe "
1006             "a hole.");
1007 
1008         // Transport the covariance to the surface
1009         stepper.transportCovarianceToBound(state.stepping, *surface,
1010                                            freeToBoundCorrection);
1011 
1012         // TODO generalize the update of the currentTrackIndex
1013         auto& fittedStates = *result.fittedStates;
1014 
1015         // Add a <trackStateMask> TrackState entry multi trajectory. This
1016         // allocates storage for all components, which we will set later.
1017         typename traj_t::TrackStateProxy trackStateProxy =
1018             fittedStates.makeTrackState(Gx2fConstants::trackStateMask,
1019                                         result.lastTrackIndex);
1020         const std::size_t currentTrackIndex = trackStateProxy.index();
1021 
1022         // Set the trackStateProxy components with the state from the ongoing
1023         // propagation
1024         {
1025           trackStateProxy.setReferenceSurface(surface->getSharedPtr());
1026           // Bind the transported state to the current surface
1027           auto res = stepper.boundState(state.stepping, *surface, false,
1028                                         freeToBoundCorrection);
1029           if (!res.ok()) {
1030             return res.error();
1031           }
1032           // Not const since, we might need to update with scattering angles
1033           auto& [boundParams, jacobian, pathLength] = *res;
1034 
1035           // For material surfaces, we also update the angles with the
1036           // available scattering information
1037           // We can skip the if here, since we already know, that we do
1038           // multipleScattering and have material
1039           ACTS_DEBUG("    Update parameters with scattering angles.");
1040           const auto scatteringMapId = scatteringMap->find(geoId);
1041           ACTS_VERBOSE(
1042               "        scatteringAngles: "
1043               << scatteringMapId->second.scatteringAngles().transpose());
1044           ACTS_VERBOSE("        boundParams before the update: "
1045                        << boundParams.parameters().transpose());
1046           boundParams.parameters() +=
1047               scatteringMapId->second.scatteringAngles();
1048           ACTS_VERBOSE("        boundParams after the update: "
1049                        << boundParams.parameters().transpose());
1050 
1051           // Fill the track state
1052           trackStateProxy.smoothed() = boundParams.parameters();
1053           trackStateProxy.smoothedCovariance() = state.stepping.cov;
1054 
1055           trackStateProxy.jacobian() = jacobian;
1056           trackStateProxy.pathLength() = pathLength;
1057 
1058           stepper.update(state.stepping,
1059                          transformBoundToFreeParameters(
1060                              trackStateProxy.referenceSurface(),
1061                              state.geoContext, trackStateProxy.smoothed()),
1062                          trackStateProxy.smoothed(),
1063                          trackStateProxy.smoothedCovariance(), *surface);
1064         }
1065 
1066         // Get and set the type flags
1067         auto typeFlags = trackStateProxy.typeFlags();
1068         typeFlags.setHasParameters();
1069         typeFlags.setHasMaterial();
1070 
1071         // Set hole only, if we are on a sensitive surface and had
1072         // measurements before (no holes before the first measurement)
1073         const bool precedingMeasurementExists = (result.measurementStates > 0);
1074         if (surfaceIsSensitive && precedingMeasurementExists) {
1075           ACTS_DEBUG("    Surface is also sensitive. Marked as hole.");
1076           typeFlags.setIsHole();
1077 
1078           // Count the missed surface
1079           result.missedActiveSurfaces.push_back(surface);
1080         }
1081 
1082         result.lastTrackIndex = currentTrackIndex;
1083 
1084         ++result.processedStates;
1085 
1086         return Result<void>::success();
1087       }
1088 
1089       if (surfaceIsSensitive || surfaceHasMaterial) {
1090         // Here we handle holes. If material hasn't been handled before
1091         // (because multipleScattering is turned off), we will also handle it
1092         // here
1093         if (multipleScattering) {
1094           ACTS_DEBUG(
1095               "    The surface contains no measurement, but maybe a hole.");
1096         } else {
1097           ACTS_DEBUG(
1098               "    The surface contains no measurement, but maybe a hole "
1099               "and/or material.");
1100         }
1101 
1102         // We only create track states here if there is already a measurement
1103         // detected (no holes before the first measurement) or if we encounter
1104         // material
1105         const bool precedingMeasurementExists = (result.measurementStates > 0);
1106         if (!precedingMeasurementExists && !surfaceHasMaterial) {
1107           ACTS_DEBUG(
1108               "    Ignoring hole, because there are no preceding "
1109               "measurements.");
1110           return Result<void>::success();
1111         }
1112 
1113         auto& fittedStates = *result.fittedStates;
1114 
1115         // Add a <trackStateMask> TrackState entry multi trajectory. This
1116         // allocates storage for all components, which we will set later.
1117         typename traj_t::TrackStateProxy trackStateProxy =
1118             fittedStates.makeTrackState(Gx2fConstants::trackStateMask,
1119                                         result.lastTrackIndex);
1120         const std::size_t currentTrackIndex = trackStateProxy.index();
1121 
1122         // Set the trackStateProxy components with the state from the
1123         // ongoing propagation
1124         {
1125           trackStateProxy.setReferenceSurface(surface->getSharedPtr());
1126           // Bind the transported state to the current surface
1127           auto res = stepper.boundState(state.stepping, *surface, false,
1128                                         freeToBoundCorrection);
1129           if (!res.ok()) {
1130             return res.error();
1131           }
1132           const auto& [boundParams, jacobian, pathLength] = *res;
1133 
1134           // Fill the track state
1135           trackStateProxy.smoothed() = boundParams.parameters();
1136           trackStateProxy.smoothedCovariance() = state.stepping.cov;
1137 
1138           trackStateProxy.jacobian() = jacobian;
1139           trackStateProxy.pathLength() = pathLength;
1140         }
1141 
1142         // Get and set the type flags
1143         auto typeFlags = trackStateProxy.typeFlags();
1144         typeFlags.setHasParameters();
1145         if (surfaceHasMaterial) {
1146           ACTS_DEBUG("    It is material.");
1147           typeFlags.setHasMaterial();
1148         }
1149 
1150         // Set hole only, if we are on a sensitive surface
1151         if (surfaceIsSensitive && precedingMeasurementExists) {
1152           ACTS_DEBUG("    It is a hole.");
1153           typeFlags.setIsHole();
1154           // Count the missed surface
1155           result.missedActiveSurfaces.push_back(surface);
1156         }
1157 
1158         result.lastTrackIndex = currentTrackIndex;
1159 
1160         ++result.processedStates;
1161 
1162         return Result<void>::success();
1163       }
1164 
1165       ACTS_DEBUG("    The surface contains no measurement/material/hole.");
1166       return Result<void>::success();
1167     }
1168 
1169     template <typename propagator_state_t, typename stepper_t,
1170               typename navigator_t, typename result_t>
1171     bool checkAbort(propagator_state_t& /*state*/, const stepper_t& /*stepper*/,
1172                     const navigator_t& /*navigator*/, const result_t& result,
1173                     const Logger& /*logger*/) const {
1174       if (result.finished) {
1175         return true;
1176       }
1177       return false;
1178     }
1179   };
1180 
1181  public:
1182   /// Fit implementation
1183   ///
1184   /// @tparam source_link_iterator_t Iterator type used to pass source links
1185   /// @tparam track_container_t Type of the track container backend
1186   /// @tparam holder_t Type defining track container backend ownership
1187   ///
1188   /// @param it Begin iterator for the fittable uncalibrated measurements
1189   /// @param end End iterator for the fittable uncalibrated measurements
1190   /// @param sParameters The initial track parameters
1191   /// @param gx2fOptions Gx2FitterOptions steering the fit
1192   /// @param trackContainer Input track container storage to append into
1193   /// @note The input measurements are given in the form of @c SourceLink s.
1194   /// It's the calibrators job to turn them into calibrated measurements used in
1195   /// the fit.
1196   ///
1197   /// @return the output as an output track
1198   template <typename source_link_iterator_t,
1199             TrackContainerFrontend track_container_t>
1200   Result<typename track_container_t::TrackProxy> fit(
1201       source_link_iterator_t it, source_link_iterator_t end,
1202       const BoundTrackParameters& sParameters,
1203       const Gx2FitterOptions<traj_t>& gx2fOptions,
1204       track_container_t& trackContainer) const
1205     requires(!isDirectNavigator)
1206   {
1207     // Preprocess Measurements (SourceLinks -> map)
1208     // To be able to find measurements later, we put them into a map.
1209     // We need to copy input SourceLinks anyway, so the map can own them.
1210     ACTS_VERBOSE("Preparing " << std::distance(it, end)
1211                               << " input measurements");
1212     std::unordered_map<const Surface*, SourceLink> inputMeasurements{};
1213 
1214     for (; it != end; ++it) {
1215       inputMeasurements.try_emplace(gx2fOptions.extensions.surfaceAccessor(*it),
1216                                     *it);
1217     }
1218 
1219     // Store, if we want to do multiple scattering. We still need to pass this
1220     // option to the Actor.
1221     const bool multipleScattering = gx2fOptions.multipleScattering;
1222 
1223     // Create the ActorList
1224     using GX2FActor = Actor;
1225 
1226     using GX2FResult = typename GX2FActor::result_type;
1227     using Actors = Acts::ActorList<GX2FActor>;
1228 
1229     using PropagatorOptions = typename propagator_t::template Options<Actors>;
1230 
1231     BoundTrackParameters params = sParameters;
1232     double chi2sum = 0;
1233     double oldChi2sum = std::numeric_limits<double>::max();
1234 
1235     // We need to create a temporary track container. We create several times a
1236     // new track and delete it after updating the parameters. However, if we
1237     // would work on the externally provided track container, it would be
1238     // difficult to remove the correct track, if it contains more than one.
1239     typename track_container_t::TrackContainerBackend trackContainerTempBackend;
1240     traj_t trajectoryTempBackend;
1241     TrackContainer trackContainerTemp{trackContainerTempBackend,
1242                                       trajectoryTempBackend};
1243 
1244     // Create an index of the 'tip' of the track stored in multitrajectory. It
1245     // is needed outside the update loop. It will be updated with each iteration
1246     // and used for the final track
1247     std::size_t tipIndex = kInvalid;
1248 
1249     // The scatteringMap stores for each visited surface their scattering
1250     // properties
1251     std::unordered_map<GeometryIdentifier, ScatteringProperties> scatteringMap;
1252 
1253     // This will be filled during the updates with the final covariance of the
1254     // track parameters.
1255     BoundMatrix fullCovariancePredicted = BoundMatrix::Identity();
1256 
1257     ACTS_VERBOSE("Initial parameters: " << params.parameters().transpose());
1258 
1259     /// Actual Fitting /////////////////////////////////////////////////////////
1260     ACTS_DEBUG("Start to iterate");
1261 
1262     // Iterate the fit and improve result. Abort after n steps or after
1263     // convergence.
1264     // nUpdate is initialized outside to save its state for the track.
1265     std::size_t nUpdate = 0;
1266     for (nUpdate = 0; nUpdate < gx2fOptions.nUpdateMax; nUpdate++) {
1267       ACTS_DEBUG("nUpdate = " << nUpdate + 1 << "/" << gx2fOptions.nUpdateMax);
1268 
1269       // set up propagator and co
1270       PropagatorOptions propagatorOptions{gx2fOptions.propagatorPlainOptions};
1271 
1272       // Add the measurement surface as external surface to the navigator.
1273       // We will try to hit those surface by ignoring boundary checks.
1274       for (const auto& [surface, _] : inputMeasurements) {
1275         propagatorOptions.navigation.appendExternalSurface(*surface);
1276       }
1277 
1278       auto& gx2fActor = propagatorOptions.actorList.template get<GX2FActor>();
1279       gx2fActor.inputMeasurements = &inputMeasurements;
1280       gx2fActor.multipleScattering = false;
1281       gx2fActor.extensions = gx2fOptions.extensions;
1282       gx2fActor.calibrationContext = &gx2fOptions.calibrationContext.get();
1283       gx2fActor.actorLogger = m_actorLogger.get();
1284       gx2fActor.scatteringMap = &scatteringMap;
1285       gx2fActor.parametersWithHypothesis = &params;
1286 
1287       auto propagatorState = m_propagator.makeState(propagatorOptions);
1288 
1289       auto propagatorInitResult =
1290           m_propagator.initialize(propagatorState, params);
1291       if (!propagatorInitResult.ok()) {
1292         ACTS_DEBUG("Propagation initialization failed: "
1293                    << propagatorInitResult.error());
1294         return propagatorInitResult.error();
1295       }
1296 
1297       auto& r = propagatorState.template get<Gx2FitterResult<traj_t>>();
1298       r.fittedStates = &trajectoryTempBackend;
1299 
1300       // Clear the track container. It could be more performant to update the
1301       // existing states, but this needs some more thinking.
1302       trackContainerTemp.clear();
1303 
1304       // Run the fitter
1305       auto propagationResult = m_propagator.propagate(propagatorState);
1306 
1307       auto result =
1308           m_propagator.makeResult(std::move(propagatorState), propagationResult,
1309                                   propagatorOptions, false);
1310 
1311       if (!result.ok()) {
1312         ACTS_DEBUG("Propagation failed: " << result.error());
1313         return result.error();
1314       }
1315 
1316       // TODO Improve Propagator + Actor [allocate before loop], rewrite
1317       // makeMeasurements
1318       auto& propRes = *result;
1319       GX2FResult gx2fResult = std::move(propRes.template get<GX2FResult>());
1320 
1321       auto track = trackContainerTemp.makeTrack();
1322       tipIndex = gx2fResult.lastMeasurementIndex;
1323 
1324       // It could happen, that no measurements were found. Then the track would
1325       // be empty and the following operations would be invalid. Usually, this
1326       // only happens during the first iteration, due to bad initial parameters.
1327       if (tipIndex == kInvalid) {
1328         ACTS_INFO("Did not find any measurements in nUpdate "
1329                   << nUpdate + 1 << "/" << gx2fOptions.nUpdateMax);
1330         return Experimental::GlobalChiSquareFitterError::NotEnoughMeasurements;
1331       }
1332 
1333       track.tipIndex() = tipIndex;
1334       track.linkForward();
1335 
1336       // Count the material surfaces, to set up the system. In the multiple
1337       // scattering case, we need to extend our system.
1338       const std::size_t nMaterialSurfaces = 0u;
1339 
1340       // We need 6 dimensions for the bound parameters and 2 * nMaterialSurfaces
1341       // dimensions for the scattering angles.
1342       const std::size_t dimsExtendedParams = eBoundSize + 2 * nMaterialSurfaces;
1343 
1344       // System that we fill with the information gathered by the actor and
1345       // evaluate later
1346       Gx2fSystem extendedSystem{dimsExtendedParams};
1347 
1348       // This vector stores the IDs for each visited material in order. We use
1349       // it later for updating the scattering angles. We cannot use
1350       // scatteringMap directly, since we cannot guarantee, that we will visit
1351       // all stored material in each propagation.
1352       std::vector<GeometryIdentifier> geoIdVector;
1353 
1354       fillGx2fSystem(track, extendedSystem, false, scatteringMap, geoIdVector,
1355                      *m_addToSumLogger);
1356 
1357       chi2sum = extendedSystem.chi2();
1358 
1359       // This check takes into account the evaluated dimensions of the
1360       // measurements. To fit, we need at least NDF+1 measurements. However, we
1361       // count n-dimensional measurements for n measurements, reducing the
1362       // effective number of needed measurements. We might encounter the case,
1363       // where we cannot use some (parts of a) measurements, maybe if we do not
1364       // support that kind of measurement. This is also taken into account here.
1365       // We skip the check during the first iteration, since we cannot guarantee
1366       // to hit all/enough measurement surfaces with the initial parameter
1367       // guess.
1368       // We skip the check during the first iteration, since we cannot guarantee
1369       // to hit all/enough measurement surfaces with the initial parameter
1370       // guess.
1371       if ((nUpdate > 0) && !extendedSystem.isWellDefined()) {
1372         ACTS_INFO("Not enough measurements. Require "
1373                   << extendedSystem.findRequiredNdf() + 1 << ", but only "
1374                   << extendedSystem.ndf() << " could be used.");
1375         return Experimental::GlobalChiSquareFitterError::NotEnoughMeasurements;
1376       }
1377 
1378       Eigen::VectorXd deltaParamsExtended =
1379           computeGx2fDeltaParams(extendedSystem);
1380 
1381       ACTS_VERBOSE("aMatrix:\n"
1382                    << extendedSystem.aMatrix() << "\n"
1383                    << "bVector:\n"
1384                    << extendedSystem.bVector() << "\n"
1385                    << "deltaParamsExtended:\n"
1386                    << deltaParamsExtended << "\n"
1387                    << "oldChi2sum = " << oldChi2sum << "\n"
1388                    << "chi2sum = " << extendedSystem.chi2());
1389 
1390       if ((gx2fOptions.relChi2changeCutOff != 0) && (nUpdate > 0) &&
1391           (std::abs(extendedSystem.chi2() / oldChi2sum - 1) <
1392            gx2fOptions.relChi2changeCutOff)) {
1393         ACTS_DEBUG("Abort with relChi2changeCutOff after "
1394                    << nUpdate + 1 << "/" << gx2fOptions.nUpdateMax
1395                    << " iterations.");
1396         updateGx2fCovarianceParams(fullCovariancePredicted, extendedSystem);
1397         break;
1398       }
1399 
1400       if (extendedSystem.chi2() > oldChi2sum + 1e-5) {
1401         ACTS_DEBUG("chi2 not converging monotonically in update " << nUpdate);
1402       }
1403 
1404       // If this is the final iteration, update the covariance and break.
1405       // Otherwise, we would update the scattering angles too much.
1406       if (nUpdate == gx2fOptions.nUpdateMax - 1) {
1407         // Since currently most of our tracks converge in 4-5 updates, we want
1408         // to set nUpdateMax higher than that to guarantee convergence for most
1409         // tracks. In cases, where we set a smaller nUpdateMax, it's because we
1410         // want to investigate the behaviour of the fitter before it converges,
1411         // like in some unit-tests.
1412         if (gx2fOptions.nUpdateMax > 5) {
1413           ACTS_INFO("Did not converge in " << gx2fOptions.nUpdateMax
1414                                            << " updates.");
1415           return Experimental::GlobalChiSquareFitterError::DidNotConverge;
1416         }
1417 
1418         updateGx2fCovarianceParams(fullCovariancePredicted, extendedSystem);
1419         break;
1420       }
1421 
1422       updateGx2fParams(params, deltaParamsExtended, nMaterialSurfaces,
1423                        scatteringMap, geoIdVector);
1424       ACTS_VERBOSE("Updated parameters: " << params.parameters().transpose());
1425 
1426       oldChi2sum = extendedSystem.chi2();
1427     }
1428     ACTS_DEBUG("Finished to iterate");
1429     ACTS_VERBOSE("Final parameters: " << params.parameters().transpose());
1430     /// Finish Fitting /////////////////////////////////////////////////////////
1431 
1432     /// Actual MATERIAL Fitting ////////////////////////////////////////////////
1433     ACTS_DEBUG("Start to evaluate material");
1434     if (multipleScattering) {
1435       // Set up the propagator
1436       PropagatorOptions propagatorOptions{gx2fOptions.propagatorPlainOptions};
1437 
1438       // Add the measurement surface as external surface to the navigator.
1439       // We will try to hit those surface by ignoring boundary checks.
1440       for (const auto& [surface, _] : inputMeasurements) {
1441         propagatorOptions.navigation.appendExternalSurface(*surface);
1442       }
1443 
1444       auto& gx2fActor = propagatorOptions.actorList.template get<GX2FActor>();
1445       gx2fActor.inputMeasurements = &inputMeasurements;
1446       gx2fActor.multipleScattering = true;
1447       gx2fActor.extensions = gx2fOptions.extensions;
1448       gx2fActor.calibrationContext = &gx2fOptions.calibrationContext.get();
1449       gx2fActor.actorLogger = m_actorLogger.get();
1450       gx2fActor.scatteringMap = &scatteringMap;
1451       gx2fActor.parametersWithHypothesis = &params;
1452 
1453       auto propagatorState = m_propagator.makeState(propagatorOptions);
1454 
1455       auto propagatorInitResult =
1456           m_propagator.initialize(propagatorState, params);
1457       if (!propagatorInitResult.ok()) {
1458         ACTS_DEBUG("Propagation initialization failed: "
1459                    << propagatorInitResult.error());
1460         return propagatorInitResult.error();
1461       }
1462 
1463       auto& r = propagatorState.template get<Gx2FitterResult<traj_t>>();
1464       r.fittedStates = &trajectoryTempBackend;
1465 
1466       // Clear the track container. It could be more performant to update the
1467       // existing states, but this needs some more thinking.
1468       trackContainerTemp.clear();
1469 
1470       // Run the fitter
1471       auto propagationResult = m_propagator.propagate(propagatorState);
1472 
1473       auto result =
1474           m_propagator.makeResult(std::move(propagatorState), propagationResult,
1475                                   propagatorOptions, false);
1476 
1477       if (!result.ok()) {
1478         ACTS_DEBUG("Propagation failed: " << result.error());
1479         return result.error();
1480       }
1481 
1482       // TODO Improve Propagator + Actor [allocate before loop], rewrite
1483       // makeMeasurements
1484       auto& propRes = *result;
1485       GX2FResult gx2fResult = std::move(propRes.template get<GX2FResult>());
1486 
1487       auto track = trackContainerTemp.makeTrack();
1488       tipIndex = gx2fResult.lastMeasurementIndex;
1489 
1490       // It could happen, that no measurements were found. Then the track would
1491       // be empty and the following operations would be invalid. Usually, this
1492       // only happens during the first iteration, due to bad initial parameters.
1493       if (tipIndex == kInvalid) {
1494         ACTS_INFO("Did not find any measurements in material fit.");
1495         return Experimental::GlobalChiSquareFitterError::NotEnoughMeasurements;
1496       }
1497 
1498       track.tipIndex() = tipIndex;
1499       track.linkForward();
1500 
1501       // Count the material surfaces, to set up the system. In the multiple
1502       // scattering case, we need to extend our system.
1503       const std::size_t nMaterialSurfaces =
1504           countMaterialStates(track, scatteringMap, *m_addToSumLogger);
1505 
1506       // We need 6 dimensions for the bound parameters and 2 * nMaterialSurfaces
1507       // dimensions for the scattering angles.
1508       const std::size_t dimsExtendedParams = eBoundSize + 2 * nMaterialSurfaces;
1509 
1510       // System that we fill with the information gathered by the actor and
1511       // evaluate later
1512       Gx2fSystem extendedSystem{dimsExtendedParams};
1513 
1514       // This vector stores the IDs for each visited material in order. We use
1515       // it later for updating the scattering angles. We cannot use
1516       // scatteringMap directly, since we cannot guarantee, that we will visit
1517       // all stored material in each propagation.
1518       std::vector<GeometryIdentifier> geoIdVector;
1519 
1520       fillGx2fSystem(track, extendedSystem, true, scatteringMap, geoIdVector,
1521                      *m_addToSumLogger);
1522 
1523       chi2sum = extendedSystem.chi2();
1524 
1525       // This check takes into account the evaluated dimensions of the
1526       // measurements. To fit, we need at least NDF+1 measurements. However, we
1527       // count n-dimensional measurements for n measurements, reducing the
1528       // effective number of needed measurements. We might encounter the case,
1529       // where we cannot use some (parts of a) measurements, maybe if we do not
1530       // support that kind of measurement. This is also taken into account here.
1531       // We skip the check during the first iteration, since we cannot guarantee
1532       // to hit all/enough measurement surfaces with the initial parameter
1533       // guess.
1534       if ((nUpdate > 0) && !extendedSystem.isWellDefined()) {
1535         ACTS_INFO("Not enough measurements. Require "
1536                   << extendedSystem.findRequiredNdf() + 1 << ", but only "
1537                   << extendedSystem.ndf() << " could be used.");
1538         return Experimental::GlobalChiSquareFitterError::NotEnoughMeasurements;
1539       }
1540 
1541       Eigen::VectorXd deltaParamsExtended =
1542           computeGx2fDeltaParams(extendedSystem);
1543 
1544       ACTS_VERBOSE("aMatrix:\n"
1545                    << extendedSystem.aMatrix() << "\n"
1546                    << "bVector:\n"
1547                    << extendedSystem.bVector() << "\n"
1548                    << "deltaParamsExtended:\n"
1549                    << deltaParamsExtended << "\n"
1550                    << "oldChi2sum = " << oldChi2sum << "\n"
1551                    << "chi2sum = " << extendedSystem.chi2());
1552 
1553       chi2sum = extendedSystem.chi2();
1554 
1555       updateGx2fParams(params, deltaParamsExtended, nMaterialSurfaces,
1556                        scatteringMap, geoIdVector);
1557       ACTS_VERBOSE("Updated parameters: " << params.parameters().transpose());
1558 
1559       updateGx2fCovarianceParams(fullCovariancePredicted, extendedSystem);
1560     }
1561     ACTS_DEBUG("Finished to evaluate material");
1562     ACTS_VERBOSE(
1563         "Final parameters after material: " << params.parameters().transpose());
1564     /// Finish MATERIAL Fitting ////////////////////////////////////////////////
1565 
1566     ACTS_VERBOSE("Final scattering angles:");
1567     for (const auto& [key, value] : scatteringMap) {
1568       if (!value.materialIsValid()) {
1569         continue;
1570       }
1571       const auto& angles = value.scatteringAngles();
1572       ACTS_VERBOSE("    ( " << angles[eBoundTheta] << " | " << angles[eBoundPhi]
1573                             << " )");
1574     }
1575 
1576     ACTS_VERBOSE("Final covariance:\n" << fullCovariancePredicted);
1577 
1578     // Propagate again with the final covariance matrix. This is necessary to
1579     // obtain the propagated covariance for each state.
1580     // We also need to recheck the result and find the tipIndex, because at this
1581     // step, we will not ignore the boundary checks for measurement surfaces. We
1582     // want to create trackstates only on surfaces, that we actually hit.
1583     if (gx2fOptions.nUpdateMax > 0) {
1584       ACTS_VERBOSE("Propagate with the final covariance.");
1585       // update covariance
1586       params.covariance() = fullCovariancePredicted;
1587 
1588       // set up the propagator
1589       PropagatorOptions propagatorOptions{gx2fOptions.propagatorPlainOptions};
1590       auto& gx2fActor = propagatorOptions.actorList.template get<GX2FActor>();
1591       gx2fActor.inputMeasurements = &inputMeasurements;
1592       gx2fActor.multipleScattering = multipleScattering;
1593       gx2fActor.extensions = gx2fOptions.extensions;
1594       gx2fActor.calibrationContext = &gx2fOptions.calibrationContext.get();
1595       gx2fActor.actorLogger = m_actorLogger.get();
1596       gx2fActor.scatteringMap = &scatteringMap;
1597       gx2fActor.parametersWithHypothesis = &params;
1598 
1599       auto propagatorState = m_propagator.makeState(propagatorOptions);
1600 
1601       auto propagatorInitResult =
1602           m_propagator.initialize(propagatorState, params);
1603       if (!propagatorInitResult.ok()) {
1604         ACTS_DEBUG("Propagation initialization failed: "
1605                    << propagatorInitResult.error());
1606         return propagatorInitResult.error();
1607       }
1608 
1609       auto& r = propagatorState.template get<Gx2FitterResult<traj_t>>();
1610       r.fittedStates = &trackContainer.trackStateContainer();
1611 
1612       // Run the fitter
1613       auto propagationResult = m_propagator.propagate(propagatorState);
1614 
1615       auto result =
1616           m_propagator.makeResult(std::move(propagatorState), propagationResult,
1617                                   propagatorOptions, false);
1618 
1619       if (!result.ok()) {
1620         ACTS_DEBUG("Propagation failed: " << result.error());
1621         return result.error();
1622       }
1623 
1624       auto& propRes = *result;
1625       GX2FResult gx2fResult = std::move(propRes.template get<GX2FResult>());
1626 
1627       if (tipIndex != gx2fResult.lastMeasurementIndex) {
1628         ACTS_INFO("Final fit used unreachable measurements.");
1629         tipIndex = gx2fResult.lastMeasurementIndex;
1630 
1631         // It could happen, that no measurements were found. Then the track
1632         // would be empty and the following operations would be invalid.
1633         if (tipIndex == kInvalid) {
1634           ACTS_INFO("Did not find any measurements in final propagation.");
1635           return Experimental::GlobalChiSquareFitterError::
1636               NotEnoughMeasurements;
1637         }
1638       }
1639     }
1640 
1641     if (!trackContainer.hasColumn(
1642             Acts::hashString(Gx2fConstants::gx2fnUpdateColumn))) {
1643       trackContainer.template addColumn<std::uint32_t>("Gx2fnUpdateColumn");
1644     }
1645 
1646     // Prepare track for return
1647     auto track = trackContainer.makeTrack();
1648     track.tipIndex() = tipIndex;
1649     track.parameters() = params.parameters();
1650     track.covariance() = fullCovariancePredicted;
1651     track.setReferenceSurface(params.referenceSurface().getSharedPtr());
1652 
1653     if (trackContainer.hasColumn(
1654             Acts::hashString(Gx2fConstants::gx2fnUpdateColumn))) {
1655       ACTS_DEBUG("Add nUpdate to track");
1656       track.template component<std::uint32_t>("Gx2fnUpdateColumn") =
1657           static_cast<std::uint32_t>(nUpdate);
1658     }
1659 
1660     // TODO write test for calculateTrackQuantities
1661     calculateTrackQuantities(track);
1662 
1663     // Set the chi2sum for the track summary manually, since we don't calculate
1664     // it for each state
1665     track.chi2() = chi2sum;
1666 
1667     // Return the converted Track
1668     return track;
1669   }
1670 };
1671 
1672 /// @}
1673 
1674 }  // namespace Acts::Experimental