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
0002
0003
0004
0005
0006
0007
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
0032 SquareMatrix3 crossMatrix(const Vector3& u) {
0033 SquareMatrix3 w;
0034
0035 w << 0., -u.z(), u.y(),
0036 u.z(), 0., -u.x(),
0037 -u.y(), u.x(), 0.;
0038
0039 return w;
0040 }
0041
0042
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
0055 result.dtds = fastHypot(1., mass / absMom);
0056
0057
0058 result.dtdsDqop =
0059 charge != 0. ? mass * mass * qop / (charge * charge * result.dtds) : 0.;
0060 return result;
0061 }
0062
0063
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
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
0082 jac.block<3, 3>(eFreeDir0, eFreePos0) =
0083 qop * crossMatrix(dir) * field.gradient;
0084
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 }
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
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& ,
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
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
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* ) 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
0310 const TimeDerivative time =
0311 timeDerivative(state.particleHypothesis, start[eFreeQOverP]);
0312
0313 const double stepTolerance = state.options.stepTolerance;
0314
0315
0316
0317
0318
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
0334 FreeVector end = FreeVector::Zero();
0335 FreeMatrix jacobian = FreeMatrix::Zero();
0336
0337 std::optional<FieldAndGradient> endField;
0338
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
0409
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
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
0466
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
0494
0495
0496
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
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
0530
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 }