Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2025-07-01 08:08:04

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