Back to home page

EIC code displayed by LXR

 
 

    


Warning, file /acts/Core/src/Propagator/GenericRungeKuttaStepper.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

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 #include "Acts/Propagator/GenericRungeKuttaStepper.hpp"
0010 
0011 #include "Acts/Definitions/TrackParametrization.hpp"
0012 #include "Acts/EventData/TransformationHelpers.hpp"
0013 #include "Acts/Propagator/EigenStepperError.hpp"
0014 #include "Acts/Propagator/detail/CovarianceEngine.hpp"
0015 #include "Acts/Surfaces/BoundaryTolerance.hpp"
0016 #include "Acts/Surfaces/SurfaceError.hpp"
0017 #include "Acts/Utilities/MathHelpers.hpp"
0018 
0019 #include <algorithm>
0020 #include <cmath>
0021 #include <limits>
0022 #include <stdexcept>
0023 #include <vector>
0024 
0025 namespace Acts {
0026 
0027 namespace {
0028 
0029 using FieldAndGradient = GenericRungeKuttaStepper::FieldAndGradient;
0030 
0031 /// Cross product matrix W with W * v = u x v
0032 SquareMatrix3 crossMatrix(const Vector3& u) {
0033   SquareMatrix3 w;
0034   // clang-format off
0035   w <<     0., -u.z(),  u.y(),
0036         u.z(),     0., -u.x(),
0037        -u.y(),  u.x(),     0.;
0038   // clang-format on
0039   return w;
0040 }
0041 
0042 /// dt/ds and its derivative by q/p
0043 struct TimeDerivative {
0044   double dtds = 0;
0045   double dtdsDqop = 0;
0046 };
0047 
0048 TimeDerivative timeDerivative(const ParticleHypothesis& particleHypothesis,
0049                               double qop) {
0050   const double mass = particleHypothesis.mass();
0051   const double absMom = particleHypothesis.extractMomentum(qop);
0052   const double charge = particleHypothesis.extractCharge(qop);
0053   TimeDerivative result;
0054   // time propagates along distance as 1/b = sqrt(1 + m²/p²)
0055   result.dtds = fastHypot(1., mass / absMom);
0056   // A neutral particle has a fixed momentum hypothesis, so its time does not
0057   // depend on q/p.
0058   result.dtdsDqop =
0059       charge != 0. ? mass * mass * qop / (charge * charge * result.dtds) : 0.;
0060   return result;
0061 }
0062 
0063 /// The equation of motion in vacuum, dy/ds = f(y)
0064 FreeVector motion(const FreeVector& pars, const Vector3& field, double dtds) {
0065   const Vector3 dir = pars.segment<3>(eFreeDir0);
0066   FreeVector derivative = FreeVector::Zero();
0067   derivative.segment<3>(eFreePos0) = dir;
0068   derivative[eFreeTime] = dtds;
0069   derivative.segment<3>(eFreeDir0) = pars[eFreeQOverP] * dir.cross(field);
0070   return derivative;
0071 }
0072 
0073 /// The jacobian of the equation of motion, df/dy
0074 FreeMatrix motionJacobian(const FreeVector& pars, const FieldAndGradient& field,
0075                           double dtdsDqop) {
0076   const Vector3 dir = pars.segment<3>(eFreeDir0);
0077   const double qop = pars[eFreeQOverP];
0078   FreeMatrix jac = FreeMatrix::Zero();
0079   jac.block<3, 3>(eFreePos0, eFreeDir0).setIdentity();
0080   jac(eFreeTime, eFreeQOverP) = dtdsDqop;
0081   // d(T x B)/dr = T x (dB/dr)
0082   jac.block<3, 3>(eFreeDir0, eFreePos0) =
0083       qop * crossMatrix(dir) * field.gradient;
0084   // d(T x B)/dT = -B x
0085   jac.block<3, 3>(eFreeDir0, eFreeDir0) = -qop * crossMatrix(field.field);
0086   jac.block<3, 1>(eFreeDir0, eFreeQOverP) = dir.cross(field.field);
0087   return jac;
0088 }
0089 
0090 }  // namespace
0091 
0092 GenericRungeKuttaStepper::GenericRungeKuttaStepper(
0093     std::shared_ptr<const MagneticFieldProvider> bField)
0094     : GenericRungeKuttaStepper(Config{std::move(bField)}) {}
0095 
0096 GenericRungeKuttaStepper::GenericRungeKuttaStepper(const Config& config)
0097     : m_bField(config.bField), m_tableau(config.tableau) {
0098   if (m_tableau == nullptr) {
0099     throw std::invalid_argument(
0100         "GenericRungeKuttaStepper: the tableau is not set");
0101   }
0102 }
0103 
0104 void GenericRungeKuttaStepper::checkErrorEstimation(
0105     const Options& options) const {
0106   if (options.errorEstimation == ErrorEstimation::Embedded &&
0107       !m_tableau->hasEmbedded()) {
0108     throw std::invalid_argument("GenericRungeKuttaStepper: the tableau " +
0109                                 m_tableau->name() +
0110                                 " has no embedded weights for the error "
0111                                 "estimate");
0112   }
0113 }
0114 
0115 GenericRungeKuttaStepper::State GenericRungeKuttaStepper::makeState(
0116     const Options& options) const {
0117   checkErrorEstimation(options);
0118   return State{options, m_bField->makeCache(options.magFieldContext)};
0119 }
0120 
0121 void GenericRungeKuttaStepper::initialize(State& state,
0122                                           const BoundParameters& par) const {
0123   initialize(state, par.parameters(), par.covariance(),
0124              par.particleHypothesis(), par.referenceSurface());
0125 }
0126 
0127 void GenericRungeKuttaStepper::initialize(State& state,
0128                                           const BoundVector& boundParams,
0129                                           const std::optional<BoundMatrix>& cov,
0130                                           ParticleHypothesis particleHypothesis,
0131                                           const Surface& surface) const {
0132   FreeVector freeParams = transformBoundToFreeParameters(
0133       surface, state.options.geoContext, boundParams);
0134 
0135   state.particleHypothesis = particleHypothesis;
0136 
0137   state.pathAccumulated = 0;
0138   state.nSteps = 0;
0139   state.stepSize = ConstrainedStep();
0140   state.stepSize.setAccuracy(state.options.initialStepSize);
0141   state.stepSize.setUser(state.options.maxStepSize);
0142   state.previousStepSize = 0;
0143   state.statistics = StepperStatistics();
0144 
0145   state.pars = freeParams;
0146   state.field.reset();
0147 
0148   state.cov = cov;
0149   if (state.cov.has_value()) {
0150     state.jacToGlobal = surface.boundToFreeJacobian(
0151         state.options.geoContext, freeParams.segment<3>(eFreePos0),
0152         freeParams.segment<3>(eFreeDir0));
0153     state.jacTransport = FreeMatrix::Identity();
0154     state.derivative = FreeVector::Zero();
0155   }
0156 }
0157 
0158 Result<GenericRungeKuttaStepper::BoundParameters>
0159 GenericRungeKuttaStepper::boundParameters(const State& state,
0160                                           const Surface& surface) const {
0161   return detail::boundParameters(state.options.geoContext, surface, state.pars,
0162                                  state.cov, state.particleHypothesis);
0163 }
0164 
0165 bool GenericRungeKuttaStepper::prepareCurvilinearState(State& state) const {
0166   // The derivatives are only missing if no step was executed yet.
0167   if (state.pathAccumulated != 0) {
0168     return true;
0169   }
0170 
0171   if (!state.field.has_value()) {
0172     auto fieldRes = getFieldAndGradient(state, position(state), false);
0173     if (!fieldRes.ok()) {
0174       return false;
0175     }
0176     state.field = *fieldRes;
0177     state.fieldHasGradient = false;
0178   }
0179 
0180   const TimeDerivative time =
0181       timeDerivative(state.particleHypothesis, qOverP(state));
0182   state.derivative = motion(state.pars, state.field->field, time.dtds);
0183   return true;
0184 }
0185 
0186 GenericRungeKuttaStepper::BoundParameters
0187 GenericRungeKuttaStepper::curvilinearParameters(const State& state) const {
0188   return detail::curvilinearParameters(state.pars, state.cov,
0189                                        state.particleHypothesis);
0190 }
0191 
0192 void GenericRungeKuttaStepper::update(State& state,
0193                                       const FreeVector& freeParams,
0194                                       const BoundVector& /*boundParams*/,
0195                                       const Covariance& covariance,
0196                                       const Surface& surface) const {
0197   state.pars = freeParams;
0198   state.field.reset();
0199   if (state.cov.has_value()) {
0200     state.cov = covariance;
0201   }
0202   state.jacToGlobal = surface.boundToFreeJacobian(
0203       state.options.geoContext, freeParams.template segment<3>(eFreePos0),
0204       freeParams.template segment<3>(eFreeDir0));
0205   state.jacTransport = FreeMatrix::Identity();
0206   state.derivative = FreeVector::Zero();
0207 }
0208 
0209 void GenericRungeKuttaStepper::update(State& state, const Vector3& uposition,
0210                                       const Vector3& udirection, double qop,
0211                                       double time) const {
0212   // Material interactions keep the position, so the cached field stays valid.
0213   if (uposition != position(state)) {
0214     state.field.reset();
0215   }
0216   state.pars.template segment<3>(eFreePos0) = uposition;
0217   state.pars.template segment<3>(eFreeDir0) = udirection;
0218   state.pars[eFreeTime] = time;
0219   state.pars[eFreeQOverP] = qop;
0220 }
0221 
0222 GenericRungeKuttaStepper::Jacobian
0223 GenericRungeKuttaStepper::transportToCurvilinear(State& state) const {
0224   Jacobian jacobian = Jacobian::Identity();
0225   if (!state.cov.has_value()) {
0226     return jacobian;
0227   }
0228   detail::transportCovarianceToCurvilinear(
0229       *state.cov, jacobian, state.jacTransport, state.derivative,
0230       state.jacToGlobal, std::nullopt, direction(state));
0231   return jacobian;
0232 }
0233 
0234 Result<GenericRungeKuttaStepper::Jacobian>
0235 GenericRungeKuttaStepper::transportToBound(
0236     State& state, const Surface& surface,
0237     const FreeToBoundCorrection& freeToBoundCorrection) const {
0238   if (!surface.isOnSurface(state.options.geoContext, position(state),
0239                            direction(state), BoundaryTolerance::Infinite())) {
0240     return Result<Jacobian>::failure(SurfaceError::GlobalPositionNotOnSurface);
0241   }
0242 
0243   Jacobian jacobian = Jacobian::Identity();
0244   if (!state.cov.has_value()) {
0245     return Result<Jacobian>::success(jacobian);
0246   }
0247   detail::transportCovarianceToBound(
0248       state.options.geoContext, surface, *state.cov, jacobian,
0249       state.jacTransport, state.derivative, state.jacToGlobal, std::nullopt,
0250       state.pars, freeToBoundCorrection);
0251   return Result<Jacobian>::success(jacobian);
0252 }
0253 
0254 Result<FieldAndGradient> GenericRungeKuttaStepper::getFieldAndGradient(
0255     State& state, const Vector3& pos, bool withGradient) const {
0256   if (withGradient && m_bField->providesFieldGradient()) {
0257     return m_bField->getFieldAndGradient(pos, state.fieldCache);
0258   }
0259 
0260   FieldAndGradient result;
0261   Result<Vector3> field = getField(state, pos);
0262   if (!field.ok()) {
0263     return Result<FieldAndGradient>::failure(field.error());
0264   }
0265   result.field = *field;
0266   if (!withGradient) {
0267     return Result<FieldAndGradient>::success(result);
0268   }
0269 
0270   // Central finite differences, with an error proportional to epsilon^2
0271   const double epsilon = state.options.fieldGradientEpsilon;
0272   for (std::size_t j = 0; j < 3; ++j) {
0273     Vector3 delta = Vector3::Zero();
0274     delta[j] = epsilon;
0275     Result<Vector3> plus = getField(state, pos + delta);
0276     if (!plus.ok()) {
0277       return Result<FieldAndGradient>::failure(plus.error());
0278     }
0279     Result<Vector3> minus = getField(state, pos - delta);
0280     if (!minus.ok()) {
0281       return Result<FieldAndGradient>::failure(minus.error());
0282     }
0283     result.gradient.col(j) = (*plus - *minus) / (2. * epsilon);
0284   }
0285   return Result<FieldAndGradient>::success(result);
0286 }
0287 
0288 Result<double> GenericRungeKuttaStepper::step(
0289     State& state, Direction propDir,
0290     const IVolumeMaterial* /*material*/) const {
0291   checkErrorEstimation(state.options);
0292 
0293   const ButcherTableau& tableau = *m_tableau;
0294   const std::size_t nStages = tableau.stages();
0295   const bool withJacobian = state.cov.has_value();
0296   const bool withGradient = withJacobian && state.options.includeFieldGradient;
0297   const ErrorEstimation errorEstimation = state.options.errorEstimation;
0298 
0299   if (!state.field.has_value() || (withGradient && !state.fieldHasGradient)) {
0300     auto fieldRes = getFieldAndGradient(state, position(state), withGradient);
0301     if (!fieldRes.ok()) {
0302       return fieldRes.error();
0303     }
0304     state.field = *fieldRes;
0305     state.fieldHasGradient = withGradient;
0306   }
0307   const FieldAndGradient startField = *state.field;
0308   const FreeVector start = state.pars;
0309   // q/p does not change in vacuum, so neither does dt/ds
0310   const TimeDerivative time =
0311       timeDerivative(state.particleHypothesis, start[eFreeQOverP]);
0312 
0313   const double stepTolerance = state.options.stepTolerance;
0314   // Step size control of Hairer, Nørsett, Wanner, Solving Ordinary
0315   // Differential Equations I, 2nd ed., Section II.4: an error estimate of
0316   // order q scales with h^(q+1). The safety factor 0.9 and the maximum growth
0317   // 5 are in the ranges recommended there. The minimum 0.2 is the value of
0318   // their DOPRI5 code.
0319   const unsigned estimateOrder =
0320       errorEstimation == ErrorEstimation::Embedded
0321           ? std::min(tableau.order(), tableau.embeddedOrder())
0322           : tableau.order();
0323   const auto calcStepSizeScaling = [&](const double errorEstimate) -> double {
0324     constexpr double safety = 0.9;
0325     constexpr double lower = 0.2;
0326     constexpr double upper = 5.0;
0327     const double exponent = 1. / (estimateOrder + 1);
0328     const double x = safety * std::pow(stepTolerance / errorEstimate, exponent);
0329     return std::clamp(x, lower, upper);
0330   };
0331 
0332   struct TableauStep {
0333     // With the normalised direction and its derivative
0334     FreeVector end = FreeVector::Zero();
0335     FreeMatrix jacobian = FreeMatrix::Zero();
0336     // Only for an FSAL tableau
0337     std::optional<FieldAndGradient> endField;
0338     // Solution minus embedded solution, before the normalisation
0339     FreeVector embeddedDiff = FreeVector::Zero();
0340     FreeMatrix embeddedJacDiff = FreeMatrix::Zero();
0341   };
0342 
0343   std::vector<FreeVector> k(nStages);
0344   std::vector<FreeMatrix> dk(withJacobian ? nStages : 0);
0345 
0346   const auto tableauStep = [&](const FreeVector& pars,
0347                                const FieldAndGradient& parsField,
0348                                const double h) -> Result<TableauStep> {
0349     TableauStep result;
0350 
0351     FieldAndGradient lastField = parsField;
0352     for (std::size_t i = 0; i < nStages; ++i) {
0353       FreeVector stage = pars;
0354       FreeMatrix dStage;
0355       if (withJacobian) {
0356         dStage.setIdentity();
0357       }
0358       for (std::size_t j = 0; j < i; ++j) {
0359         const double aij = tableau.a(i, j);
0360         if (aij == 0.) {
0361           continue;
0362         }
0363         stage += h * aij * k[j];
0364         if (withJacobian) {
0365           dStage += h * aij * dk[j];
0366         }
0367       }
0368 
0369       FieldAndGradient field = parsField;
0370       if (i > 0) {
0371         auto fieldRes = getFieldAndGradient(state, stage.segment<3>(eFreePos0),
0372                                             withGradient);
0373         if (!fieldRes.ok()) {
0374           return fieldRes.error();
0375         }
0376         field = *fieldRes;
0377       }
0378       lastField = field;
0379 
0380       k[i] = motion(stage, field.field, time.dtds);
0381       if (withJacobian) {
0382         dk[i] = motionJacobian(stage, field, time.dtdsDqop) * dStage;
0383       }
0384     }
0385 
0386     result.end = pars;
0387     for (std::size_t i = 0; i < nStages; ++i) {
0388       result.end += h * tableau.b(i) * k[i];
0389     }
0390     if (tableau.firstSameAsLast()) {
0391       result.endField = lastField;
0392     }
0393 
0394     if (errorEstimation == ErrorEstimation::Embedded) {
0395       result.embeddedDiff = FreeVector::Zero();
0396       for (std::size_t i = 0; i < nStages; ++i) {
0397         result.embeddedDiff += h * (tableau.b(i) - tableau.bEmbedded(i)) * k[i];
0398       }
0399       if (withJacobian && state.options.jacobianInErrorEstimate) {
0400         result.embeddedJacDiff = FreeMatrix::Zero();
0401         for (std::size_t i = 0; i < nStages; ++i) {
0402           result.embeddedJacDiff +=
0403               h * (tableau.b(i) - tableau.bEmbedded(i)) * dk[i];
0404         }
0405       }
0406     }
0407 
0408     // The direction is normalised after the step, with the derivative
0409     // d(T/|T|)/dT = (I - T T^T / |T|^2) / |T|.
0410     const Vector3 endDir = result.end.segment<3>(eFreeDir0);
0411     const double endDirNorm = endDir.norm();
0412     if (withJacobian) {
0413       result.jacobian = FreeMatrix::Identity();
0414       for (std::size_t i = 0; i < nStages; ++i) {
0415         result.jacobian += h * tableau.b(i) * dk[i];
0416       }
0417       const Vector3 unitDir = endDir / endDirNorm;
0418       const SquareMatrix3 normalisation =
0419           (SquareMatrix3::Identity() - unitDir * unitDir.transpose()) /
0420           endDirNorm;
0421       result.jacobian.middleRows<3>(eFreeDir0) =
0422           (normalisation * result.jacobian.middleRows<3>(eFreeDir0)).eval();
0423     }
0424     result.end.segment<3>(eFreeDir0) /= endDirNorm;
0425 
0426     return result;
0427   };
0428 
0429   // q/p does not change in vacuum, so it does not contribute
0430   const auto errorNorm = [](const FreeVector& diff) {
0431     return std::max({diff.segment<3>(eFreePos0).cwiseAbs().maxCoeff(),
0432                      std::abs(diff[eFreeTime]),
0433                      diff.segment<3>(eFreeDir0).cwiseAbs().maxCoeff()});
0434   };
0435 
0436   const double initialH = state.stepSize.value() * propDir;
0437   double h = initialH;
0438 
0439   TableauStep result;
0440   double errorEstimate = 0.;
0441   std::size_t nStepTrials = 0;
0442 
0443   while (true) {
0444     ++nStepTrials;
0445     ++state.statistics.nAttemptedSteps;
0446 
0447     auto fullStep = tableauStep(start, startField, h);
0448     if (!fullStep.ok()) {
0449       return fullStep.error();
0450     }
0451 
0452     if (errorEstimation == ErrorEstimation::None) {
0453       result = std::move(*fullStep);
0454       break;
0455     }
0456 
0457     if (errorEstimation == ErrorEstimation::Embedded) {
0458       result = std::move(*fullStep);
0459       errorEstimate = errorNorm(result.embeddedDiff);
0460       if (withJacobian && state.options.jacobianInErrorEstimate) {
0461         errorEstimate = std::max(errorEstimate,
0462                                  result.embeddedJacDiff.cwiseAbs().maxCoeff());
0463       }
0464     } else {
0465       // Hairer, Nørsett, Wanner, Section II.4: the error of the two half
0466       // steps is (y_{h/2} - y_h) / (2^p - 1) to leading order.
0467       auto firstHalf = tableauStep(start, startField, h / 2);
0468       if (!firstHalf.ok()) {
0469         return firstHalf.error();
0470       }
0471       FieldAndGradient midField;
0472       if (firstHalf->endField.has_value()) {
0473         midField = *firstHalf->endField;
0474       } else {
0475         auto fieldRes = getFieldAndGradient(
0476             state, firstHalf->end.segment<3>(eFreePos0), withGradient);
0477         if (!fieldRes.ok()) {
0478           return fieldRes.error();
0479         }
0480         midField = *fieldRes;
0481       }
0482       auto secondHalf = tableauStep(firstHalf->end, midField, h / 2);
0483       if (!secondHalf.ok()) {
0484         return secondHalf.error();
0485       }
0486 
0487       result = std::move(*secondHalf);
0488       const double scale = 1. / (std::pow(2., tableau.order()) - 1.);
0489       errorEstimate = scale * errorNorm(result.end - fullStep->end);
0490       if (withJacobian) {
0491         result.jacobian = (result.jacobian * firstHalf->jacobian).eval();
0492         if (state.options.jacobianInErrorEstimate) {
0493           // Each half step projects out the direction perturbations along
0494           // the direction, but the full step does not, so the two paths
0495           // differ at O(h) there. The bound parameters do not have these
0496           // perturbations, so leave them out.
0497           const Vector3 startDir = start.segment<3>(eFreeDir0);
0498           FreeMatrix projection = FreeMatrix::Identity();
0499           projection.block<3, 3>(eFreeDir0, eFreeDir0) -=
0500               startDir * startDir.transpose();
0501           const FreeMatrix jacDiff =
0502               (result.jacobian - fullStep->jacobian) * projection;
0503           errorEstimate =
0504               std::max(errorEstimate, scale * jacDiff.cwiseAbs().maxCoeff());
0505         }
0506       }
0507     }
0508 
0509     // Avoid a division by zero in the step size scaling
0510     errorEstimate = std::max(errorEstimate, std::numeric_limits<double>::min());
0511 
0512     if (errorEstimate <= stepTolerance) {
0513       break;
0514     }
0515 
0516     ++state.statistics.nRejectedSteps;
0517 
0518     h *= calcStepSizeScaling(errorEstimate);
0519 
0520     if (std::abs(h) < std::abs(state.options.stepSizeCutOff)) {
0521       return EigenStepperError::StepSizeStalled;
0522     }
0523 
0524     if (nStepTrials > state.options.maxRungeKuttaStepTrials) {
0525       return EigenStepperError::StepSizeAdjustmentFailed;
0526     }
0527   }
0528 
0529   // The field at the end is the first stage of the next step. Look it up
0530   // before the state changes, so that a failure leaves the state unchanged.
0531   if (!result.endField.has_value()) {
0532     auto endField = getFieldAndGradient(state, result.end.segment<3>(eFreePos0),
0533                                         withGradient);
0534     if (!endField.ok()) {
0535       return endField.error();
0536     }
0537     result.endField = *endField;
0538   }
0539 
0540   if (withJacobian) {
0541     state.jacTransport = (result.jacobian * state.jacTransport).eval();
0542   }
0543 
0544   state.pars = result.end;
0545   state.field = result.endField;
0546   state.fieldHasGradient = withGradient;
0547 
0548   if (withJacobian) {
0549     state.derivative = motion(state.pars, state.field->field, time.dtds);
0550   }
0551 
0552   state.pathAccumulated += h;
0553   ++state.nSteps;
0554 
0555   ++state.statistics.nSuccessfulSteps;
0556   if (propDir != Direction::fromScalarZeroAsPositive(initialH)) {
0557     ++state.statistics.nReverseSteps;
0558   }
0559   state.statistics.pathLength += h;
0560   state.statistics.absolutePathLength += std::abs(h);
0561 
0562   if (errorEstimation != ErrorEstimation::None) {
0563     const double nextAccuracy =
0564         std::abs(h * calcStepSizeScaling(errorEstimate));
0565     const double previousAccuracy = std::abs(state.stepSize.accuracy());
0566     const double initialStepLength = std::abs(initialH);
0567     if (nextAccuracy < initialStepLength || nextAccuracy > previousAccuracy) {
0568       state.stepSize.setAccuracy(nextAccuracy);
0569     }
0570   }
0571 
0572   return h;
0573 }
0574 
0575 }  // namespace Acts