File indexing completed on 2026-10-07 08:48:36
0001
0002
0003
0004
0005
0006
0007
0008
0009 #include "Acts/Propagator/HelixStepper.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
0022 namespace Acts {
0023
0024 namespace {
0025
0026
0027
0028
0029 struct HelixCoefficients {
0030
0031 double f1 = 0;
0032
0033 double f2 = 0;
0034
0035 double g2 = 0;
0036
0037 double h1 = 0;
0038
0039 double h2 = 0;
0040 };
0041
0042 HelixCoefficients helixCoefficients(double theta2) {
0043 HelixCoefficients c;
0044
0045
0046 if (theta2 < 1e-3) {
0047 const double t2 = theta2;
0048 c.f1 = 1. + t2 * (-1. / 6. + t2 * (1. / 120. + t2 * (-1. / 5040.)));
0049 c.f2 = 1. / 2. + t2 * (-1. / 24. + t2 * (1. / 720. + t2 * (-1. / 40320.)));
0050 c.g2 =
0051 1. / 6. + t2 * (-1. / 120. + t2 * (1. / 5040. + t2 * (-1. / 362880.)));
0052 c.h1 = 1. / 3. + t2 * (-1. / 30. + t2 * (1. / 840. + t2 * (-1. / 45360.)));
0053 c.h2 =
0054 1. / 8. + t2 * (-1. / 144. + t2 * (1. / 5760. + t2 * (-1. / 403200.)));
0055 return c;
0056 }
0057 const double theta = std::sqrt(theta2);
0058
0059
0060 const double halfSin = std::sin(0.5 * theta);
0061 const double halfCos = std::cos(0.5 * theta);
0062 const double sinTheta = 2. * halfSin * halfCos;
0063 const double oneMinusCos = 2. * halfSin * halfSin;
0064 const double cosTheta = 1. - oneMinusCos;
0065 const double theta3 = theta2 * theta;
0066 c.f1 = sinTheta / theta;
0067 c.f2 = oneMinusCos / theta2;
0068 c.g2 = (theta - sinTheta) / theta3;
0069 c.h1 = (sinTheta - theta * cosTheta) / theta3;
0070 c.h2 = (0.5 * theta2 + oneMinusCos - theta * sinTheta) / (theta2 * theta2);
0071 return c;
0072 }
0073
0074
0075 SquareMatrix3 crossMatrix(const Vector3& omega) {
0076 SquareMatrix3 w;
0077
0078 w << 0., -omega.z(), omega.y(),
0079 omega.z(), 0., -omega.x(),
0080 -omega.y(), omega.x(), 0.;
0081
0082 return w;
0083 }
0084
0085 }
0086
0087 HelixStepper::HelixStepper(std::shared_ptr<const MagneticFieldProvider> bField)
0088 : m_bField(std::move(bField)) {}
0089
0090 HelixStepper::HelixStepper(const Config& config) : m_bField(config.bField) {}
0091
0092 HelixStepper::State HelixStepper::makeState(const Options& options) const {
0093 State state{options, m_bField->makeCache(options.magFieldContext)};
0094 return state;
0095 }
0096
0097 void HelixStepper::initialize(State& state, const BoundParameters& par) const {
0098 initialize(state, par.parameters(), par.covariance(),
0099 par.particleHypothesis(), par.referenceSurface());
0100 }
0101
0102 void HelixStepper::initialize(State& state, const BoundVector& boundParams,
0103 const std::optional<BoundMatrix>& cov,
0104 ParticleHypothesis particleHypothesis,
0105 const Surface& surface) const {
0106 FreeVector freeParams = transformBoundToFreeParameters(
0107 surface, state.options.geoContext, boundParams);
0108
0109 state.particleHypothesis = particleHypothesis;
0110
0111 state.pathAccumulated = 0;
0112 state.nSteps = 0;
0113 state.stepSize = ConstrainedStep();
0114 state.stepSize.setAccuracy(state.options.initialStepSize);
0115 state.stepSize.setUser(state.options.maxStepSize);
0116 state.previousStepSize = 0;
0117 state.statistics = StepperStatistics();
0118
0119 state.pars = freeParams;
0120 state.field.reset();
0121
0122 state.cov = cov;
0123 if (state.cov.has_value()) {
0124 state.jacToGlobal = surface.boundToFreeJacobian(
0125 state.options.geoContext, freeParams.segment<3>(eFreePos0),
0126 freeParams.segment<3>(eFreeDir0));
0127 state.jacTransport = FreeMatrix::Identity();
0128 state.derivative = FreeVector::Zero();
0129 }
0130 }
0131
0132 Result<HelixStepper::BoundParameters> HelixStepper::boundParameters(
0133 const State& state, const Surface& surface) const {
0134 return detail::boundParameters(state.options.geoContext, surface, state.pars,
0135 state.cov, state.particleHypothesis);
0136 }
0137
0138 bool HelixStepper::prepareCurvilinearState(State& state) const {
0139
0140 if (state.pathAccumulated != 0) {
0141 return true;
0142 }
0143
0144 if (!state.field.has_value()) {
0145 auto fieldRes = getField(state, position(state));
0146 if (!fieldRes.ok()) {
0147 return false;
0148 }
0149 state.field = *fieldRes;
0150 }
0151
0152 const double qop = qOverP(state);
0153 const double mass = state.particleHypothesis.mass();
0154 state.derivative.segment<3>(eFreePos0) = direction(state);
0155 state.derivative[eFreeTime] = fastHypot(1., mass / absoluteMomentum(state));
0156 state.derivative.segment<3>(eFreeDir0) =
0157 qop * direction(state).cross(*state.field);
0158 state.derivative[eFreeQOverP] = 0.;
0159 return true;
0160 }
0161
0162 HelixStepper::BoundParameters HelixStepper::curvilinearParameters(
0163 const State& state) const {
0164 return detail::curvilinearParameters(state.pars, state.cov,
0165 state.particleHypothesis);
0166 }
0167
0168 void HelixStepper::update(State& state, const FreeVector& freeParams,
0169 const BoundVector& ,
0170 const Covariance& covariance,
0171 const Surface& surface) const {
0172 state.pars = freeParams;
0173 state.field.reset();
0174 if (state.cov.has_value()) {
0175 state.cov = covariance;
0176 }
0177 state.jacToGlobal = surface.boundToFreeJacobian(
0178 state.options.geoContext, freeParams.template segment<3>(eFreePos0),
0179 freeParams.template segment<3>(eFreeDir0));
0180 state.jacTransport = FreeMatrix::Identity();
0181 state.derivative = FreeVector::Zero();
0182 }
0183
0184 void HelixStepper::update(State& state, const Vector3& uposition,
0185 const Vector3& udirection, double qop,
0186 double time) const {
0187
0188 if (uposition != position(state)) {
0189 state.field.reset();
0190 }
0191 state.pars.template segment<3>(eFreePos0) = uposition;
0192 state.pars.template segment<3>(eFreeDir0) = udirection;
0193 state.pars[eFreeTime] = time;
0194 state.pars[eFreeQOverP] = qop;
0195 }
0196
0197 HelixStepper::Jacobian HelixStepper::transportToCurvilinear(
0198 State& state) const {
0199 Jacobian jacobian = Jacobian::Identity();
0200 if (!state.cov.has_value()) {
0201 return jacobian;
0202 }
0203 detail::transportCovarianceToCurvilinear(
0204 *state.cov, jacobian, state.jacTransport, state.derivative,
0205 state.jacToGlobal, std::nullopt, direction(state));
0206 return jacobian;
0207 }
0208
0209 Result<HelixStepper::Jacobian> HelixStepper::transportToBound(
0210 State& state, const Surface& surface,
0211 const FreeToBoundCorrection& freeToBoundCorrection) const {
0212 if (!surface.isOnSurface(state.options.geoContext, position(state),
0213 direction(state), BoundaryTolerance::Infinite())) {
0214 return Result<Jacobian>::failure(SurfaceError::GlobalPositionNotOnSurface);
0215 }
0216
0217 Jacobian jacobian = Jacobian::Identity();
0218 if (!state.cov.has_value()) {
0219 return Result<Jacobian>::success(jacobian);
0220 }
0221 detail::transportCovarianceToBound(
0222 state.options.geoContext, surface, *state.cov, jacobian,
0223 state.jacTransport, state.derivative, state.jacToGlobal, std::nullopt,
0224 state.pars, freeToBoundCorrection);
0225 return Result<Jacobian>::success(jacobian);
0226 }
0227
0228 Result<double> HelixStepper::step(State& state, Direction propDir,
0229 const IVolumeMaterial* ) const {
0230 const Vector3 pos = position(state);
0231 const Vector3 dir = direction(state);
0232 const double qop = qOverP(state);
0233
0234 if (!state.field.has_value()) {
0235 auto fieldRes = getField(state, pos);
0236 if (!fieldRes.ok()) {
0237 return fieldRes.error();
0238 }
0239 state.field = *fieldRes;
0240 }
0241 const Vector3 startField = *state.field;
0242
0243
0244
0245
0246
0247
0248 const Vector3 omega = qop * startField;
0249 const Vector3 a = omega.cross(dir);
0250 const Vector3 b = omega.cross(a);
0251 const double omega2 = omega.squaredNorm();
0252
0253 const auto calcStepSizeScaling = [&](const double errorEstimate) -> double {
0254 constexpr double lower = 0.25;
0255 constexpr double upper = 4.0;
0256
0257 const double x = std::cbrt(state.options.stepTolerance / errorEstimate);
0258 return std::clamp(x, lower, upper);
0259 };
0260
0261 const double initialH = state.stepSize.value() * propDir;
0262 double h = initialH;
0263 HelixCoefficients coeffs;
0264 Vector3 endPos;
0265 Vector3 endDir;
0266 Vector3 endField;
0267 double errorEstimate = 0.;
0268 std::size_t nStepTrials = 0;
0269
0270 while (true) {
0271 ++nStepTrials;
0272 ++state.statistics.nAttemptedSteps;
0273
0274 coeffs = helixCoefficients(omega2 * h * h);
0275 const double h2 = h * h;
0276 endDir = dir - h * coeffs.f1 * a + h2 * coeffs.f2 * b;
0277 endPos = pos + h * dir - h2 * coeffs.f2 * a + h2 * h * coeffs.g2 * b;
0278
0279
0280
0281 auto fieldRes = getField(state, endPos);
0282 if (!fieldRes.ok()) {
0283 return fieldRes.error();
0284 }
0285 endField = *fieldRes;
0286
0287
0288 errorEstimate = std::max(
0289 1e-20, h2 / 6. * (qop * dir.cross(endField - startField)).norm());
0290
0291 if (errorEstimate <= state.options.stepTolerance) {
0292 break;
0293 }
0294
0295 ++state.statistics.nRejectedSteps;
0296
0297 h *= calcStepSizeScaling(errorEstimate);
0298
0299 if (std::abs(h) < std::abs(state.options.stepSizeCutOff)) {
0300 return EigenStepperError::StepSizeStalled;
0301 }
0302
0303 if (nStepTrials > state.options.maxRungeKuttaStepTrials) {
0304 return EigenStepperError::StepSizeAdjustmentFailed;
0305 }
0306 }
0307
0308 const double mass = state.particleHypothesis.mass();
0309 const double absMom = absoluteMomentum(state);
0310
0311 const double dtds = fastHypot(1., mass / absMom);
0312
0313 if (state.cov.has_value()) {
0314 const double h2 = h * h;
0315 const SquareMatrix3 w = crossMatrix(omega);
0316 const SquareMatrix3 w2 = w * w;
0317
0318
0319
0320 Matrix<4, 4> dTopRight = Matrix<4, 4>::Zero();
0321 Matrix<4, 4> dBottomRight = Matrix<4, 4>::Identity();
0322
0323
0324 dTopRight.topLeftCorner<3, 3>() = h * SquareMatrix3::Identity() -
0325 h2 * coeffs.f2 * w +
0326 h2 * h * coeffs.g2 * w2;
0327 dBottomRight.topLeftCorner<3, 3>() =
0328 SquareMatrix3::Identity() - h * coeffs.f1 * w + h2 * coeffs.f2 * w2;
0329
0330
0331
0332
0333
0334 dTopRight.block<3, 1>(0, 3) =
0335 (0.5 * h2 * dir - h2 * h * coeffs.h1 * a + h2 * h2 * coeffs.h2 * b)
0336 .cross(startField);
0337 dBottomRight.block<3, 1>(0, 3) = h * endDir.cross(startField);
0338
0339
0340
0341 const double q = charge(state);
0342 dTopRight(3, 3) = q != 0. ? h * mass * mass * qop / (q * q * dtds) : 0.;
0343
0344
0345 state.jacTransport.template topRightCorner<4, 4>() +=
0346 dTopRight * state.jacTransport.template bottomRightCorner<4, 4>();
0347 state.jacTransport.template bottomRightCorner<4, 4>() =
0348 (dBottomRight * state.jacTransport.template bottomRightCorner<4, 4>())
0349 .eval();
0350
0351 state.derivative.template segment<3>(eFreePos0) = endDir;
0352 state.derivative[eFreeTime] = dtds;
0353 state.derivative.template segment<3>(eFreeDir0) = endDir.cross(omega);
0354 state.derivative[eFreeQOverP] = 0.;
0355 }
0356
0357 state.pars.template segment<3>(eFreePos0) = endPos;
0358 state.pars.template segment<3>(eFreeDir0) = endDir;
0359 state.pars[eFreeTime] += h * dtds;
0360 state.field = endField;
0361
0362 state.pathAccumulated += h;
0363 ++state.nSteps;
0364
0365 ++state.statistics.nSuccessfulSteps;
0366 if (propDir != Direction::fromScalarZeroAsPositive(initialH)) {
0367 ++state.statistics.nReverseSteps;
0368 }
0369 state.statistics.pathLength += h;
0370 state.statistics.absolutePathLength += std::abs(h);
0371
0372 const double nextAccuracy = std::abs(h * calcStepSizeScaling(errorEstimate));
0373 const double previousAccuracy = std::abs(state.stepSize.accuracy());
0374 const double initialStepLength = std::abs(initialH);
0375 if (nextAccuracy < initialStepLength || nextAccuracy > previousAccuracy) {
0376 state.stepSize.setAccuracy(nextAccuracy);
0377 }
0378
0379 return h;
0380 }
0381
0382 }