Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-01-08 09:22:00

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