Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-11-14 09:33:20

0001 // This file is part of the ACTS project.
0002 //
0003 // Copyright (C) 2016 CERN for the benefit of the ACTS project
0004 //
0005 // This Source Code Form is subject to the terms of the Mozilla Public
0006 // License, v. 2.0. If a copy of the MPL was not distributed with this
0007 // file, You can obtain one at https://mozilla.org/MPL/2.0/.
0008 
0009 #pragma once
0010 
0011 #include "Acts/Definitions/Direction.hpp"
0012 #include "Acts/Definitions/Units.hpp"
0013 #include "Acts/Geometry/CuboidVolumeBuilder.hpp"
0014 #include "Acts/Geometry/GeometryContext.hpp"
0015 #include "Acts/Geometry/TrackingGeometry.hpp"
0016 #include "Acts/Geometry/TrackingGeometryBuilder.hpp"
0017 #include "Acts/MagneticField/MagneticFieldContext.hpp"
0018 #include "Acts/Material/HomogeneousVolumeMaterial.hpp"
0019 #include "Acts/Material/Interactions.hpp"
0020 #include "Acts/Material/Material.hpp"
0021 #include "Acts/Surfaces/CylinderSurface.hpp"
0022 #include "Acts/Surfaces/DiscSurface.hpp"
0023 #include "Acts/Surfaces/PlaneSurface.hpp"
0024 #include "Acts/Surfaces/RectangleBounds.hpp"
0025 #include "Acts/Surfaces/StrawSurface.hpp"
0026 #include "Acts/Utilities/Logger.hpp"
0027 #include "Acts/Utilities/MathHelpers.hpp"
0028 #include "Acts/Utilities/TransformHelpers.hpp"
0029 #include "Acts/Utilities/UnitVectors.hpp"
0030 #include "Acts/Utilities/detail/periodic.hpp"
0031 #include "ActsTests/CommonHelpers/FloatComparisons.hpp"
0032 #include "ActsTests/CommonHelpers/PredefinedMaterials.hpp"
0033 
0034 #include <utility>
0035 
0036 inline std::shared_ptr<const Acts::TrackingGeometry> createDenseBlock(
0037     const Acts::GeometryContext& geoCtx) {
0038   using namespace Acts;
0039   using namespace ActsTests;
0040   using namespace UnitLiterals;
0041 
0042   CuboidVolumeBuilder::VolumeConfig vConf;
0043   vConf.position = {0., 0., 0.};
0044   vConf.length = {4_m, 4_m, 4_m};
0045   vConf.volumeMaterial =
0046       std::make_shared<const HomogeneousVolumeMaterial>(makeBeryllium());
0047   CuboidVolumeBuilder::Config conf;
0048   conf.volumeCfg.push_back(vConf);
0049   conf.position = {0., 0., 0.};
0050   conf.length = {4_m, 4_m, 4_m};
0051   CuboidVolumeBuilder cvb(conf);
0052   TrackingGeometryBuilder::Config tgbCfg;
0053   tgbCfg.trackingVolumeBuilders.push_back(
0054       [=](const auto& context, const auto& inner, const auto&) {
0055         return cvb.trackingVolume(context, inner, nullptr);
0056       });
0057 
0058   return TrackingGeometryBuilder(tgbCfg).trackingGeometry(geoCtx);
0059 }
0060 
0061 inline std::tuple<std::shared_ptr<const Acts::TrackingGeometry>,
0062                   std::vector<const Acts::Surface*>>
0063 createDenseTelescope(const Acts::GeometryContext& geoCtx,
0064                      Acts::Material material, double thickness) {
0065   using namespace Acts;
0066   using namespace UnitLiterals;
0067 
0068   CuboidVolumeBuilder::Config conf;
0069   conf.position = {0., 0., 0.};
0070   conf.length = {4_m, 2_m, 2_m};
0071 
0072   {
0073     CuboidVolumeBuilder::VolumeConfig start;
0074     start.position = {-1_m, 0, 0};
0075     start.length = {1_m, 1_m, 1_m};
0076     start.name = "start";
0077 
0078     conf.volumeCfg.push_back(start);
0079   }
0080 
0081   if (thickness < 1_m) {
0082     CuboidVolumeBuilder::VolumeConfig gap;
0083     gap.position = {-0.25 * (1_m + thickness), 0, 0};
0084     gap.length = {0.5 * (1_m - thickness), 1_m, 1_m};
0085     gap.name = "gap1";
0086 
0087     conf.volumeCfg.push_back(gap);
0088   }
0089 
0090   {
0091     CuboidVolumeBuilder::VolumeConfig dense;
0092     dense.position = {0, 0, 0};
0093     dense.length = {thickness, 1_m, 1_m};
0094     dense.volumeMaterial =
0095         std::make_shared<const HomogeneousVolumeMaterial>(material);
0096     dense.name = "dense";
0097 
0098     conf.volumeCfg.push_back(dense);
0099   }
0100 
0101   if (thickness < 1_m) {
0102     CuboidVolumeBuilder::VolumeConfig gap;
0103     gap.position = {0.25 * (1_m + thickness), 0, 0};
0104     gap.length = {0.5 * (1_m - thickness), 1_m, 1_m};
0105     gap.name = "gap2";
0106 
0107     conf.volumeCfg.push_back(gap);
0108   }
0109 
0110   {
0111     CuboidVolumeBuilder::SurfaceConfig surface;
0112     surface.position = {1.5_m, 0, 0};
0113     surface.rotation =
0114         Eigen::AngleAxisd(std::numbers::pi / 2, Eigen::Vector3d::UnitY())
0115             .matrix();
0116     surface.rBounds = std::make_shared<RectangleBounds>(1_m, 1_m);
0117 
0118     CuboidVolumeBuilder::LayerConfig layer;
0119     layer.surfaceCfg.push_back(surface);
0120 
0121     CuboidVolumeBuilder::VolumeConfig end;
0122     end.position = {1_m, 0, 0};
0123     end.length = {1_m, 1_m, 1_m};
0124     end.layerCfg.push_back(layer);
0125     end.name = "end";
0126 
0127     conf.volumeCfg.push_back(end);
0128   }
0129 
0130   CuboidVolumeBuilder cvb(conf);
0131 
0132   TrackingGeometryBuilder::Config tgbCfg;
0133   tgbCfg.trackingVolumeBuilders.push_back(
0134       [=](const auto& context, const auto& inner, const auto&) {
0135         return cvb.trackingVolume(context, inner, nullptr);
0136       });
0137   auto detector = TrackingGeometryBuilder(tgbCfg).trackingGeometry(geoCtx);
0138 
0139   std::vector<const Surface*> surfaces;
0140   detector->visitSurfaces(
0141       [&](const Surface* surface) { surfaces.push_back(surface); });
0142 
0143   return {std::move(detector), std::move(surfaces)};
0144 }
0145 
0146 // parameter construction helpers
0147 
0148 /// Construct (initial) curvilinear parameters.
0149 inline Acts::BoundTrackParameters makeParametersCurvilinear(double phi,
0150                                                             double theta,
0151                                                             double absMom,
0152                                                             double charge) {
0153   using namespace Acts;
0154   using namespace UnitLiterals;
0155 
0156   Vector4 pos4 = Vector4::Zero();
0157   auto particleHypothesis = ParticleHypothesis::pionLike(std::abs(charge));
0158   return BoundTrackParameters::createCurvilinear(
0159       pos4, phi, theta, particleHypothesis.qOverP(absMom, charge), std::nullopt,
0160       particleHypothesis);
0161 }
0162 
0163 /// Construct (initial) curvilinear parameters with covariance.
0164 inline Acts::BoundTrackParameters makeParametersCurvilinearWithCovariance(
0165     double phi, double theta, double absMom, double charge) {
0166   using namespace Acts;
0167   using namespace UnitLiterals;
0168 
0169   BoundVector stddev = BoundVector::Zero();
0170   // TODO use momentum-dependent resolutions
0171   stddev[eBoundLoc0] = 15_um;
0172   stddev[eBoundLoc1] = 80_um;
0173   stddev[eBoundTime] = 20_ps;
0174   stddev[eBoundPhi] = 20_mrad;
0175   stddev[eBoundTheta] = 30_mrad;
0176   stddev[eBoundQOverP] = 1_e / 10_GeV;
0177   BoundMatrix corr = BoundMatrix::Identity();
0178   corr(eBoundLoc0, eBoundLoc1) = corr(eBoundLoc1, eBoundLoc0) = 0.125;
0179   corr(eBoundLoc0, eBoundPhi) = corr(eBoundPhi, eBoundLoc0) = 0.25;
0180   corr(eBoundLoc1, eBoundTheta) = corr(eBoundTheta, eBoundLoc1) = -0.25;
0181   corr(eBoundTime, eBoundQOverP) = corr(eBoundQOverP, eBoundTime) = 0.125;
0182   corr(eBoundPhi, eBoundTheta) = corr(eBoundTheta, eBoundPhi) = -0.25;
0183   corr(eBoundPhi, eBoundQOverP) = corr(eBoundPhi, eBoundQOverP) = -0.125;
0184   corr(eBoundTheta, eBoundQOverP) = corr(eBoundTheta, eBoundQOverP) = 0.5;
0185   BoundMatrix cov = stddev.asDiagonal() * corr * stddev.asDiagonal();
0186 
0187   Vector4 pos4 = Vector4::Zero();
0188   auto particleHypothesis = ParticleHypothesis::pionLike(std::abs(charge));
0189   return BoundTrackParameters::createCurvilinear(
0190       pos4, phi, theta, particleHypothesis.qOverP(absMom, charge), cov,
0191       particleHypothesis);
0192 }
0193 
0194 /// Construct (initial) neutral curvilinear parameters.
0195 inline Acts::BoundTrackParameters makeParametersCurvilinearNeutral(
0196     double phi, double theta, double absMom) {
0197   using namespace Acts;
0198   using namespace UnitLiterals;
0199 
0200   Vector4 pos4 = Vector4::Zero();
0201   return BoundTrackParameters::createCurvilinear(
0202       pos4, phi, theta, 0, std::nullopt,
0203       ParticleHypothesis::pion0().withMomentumHypothesis(absMom));
0204 }
0205 
0206 // helpers to compare track parameters
0207 
0208 /// Check that two parameters object are consistent within the tolerances.
0209 ///
0210 /// \warning Does not check that they are defined on the same surface.
0211 inline void checkParametersConsistency(const Acts::BoundTrackParameters& cmp,
0212                                        const Acts::BoundTrackParameters& ref,
0213                                        const Acts::GeometryContext& geoCtx,
0214                                        double epsPos, double epsTime,
0215                                        double epsDir, double epsMom) {
0216   using namespace Acts;
0217 
0218   // check stored parameters
0219   CHECK_CLOSE_ABS(cmp.template get<eBoundLoc0>(),
0220                   ref.template get<eBoundLoc0>(), epsPos);
0221   CHECK_CLOSE_ABS(cmp.template get<eBoundLoc1>(),
0222                   ref.template get<eBoundLoc1>(), epsPos);
0223   CHECK_CLOSE_ABS(cmp.template get<eBoundTime>(),
0224                   ref.template get<eBoundTime>(), epsTime);
0225   // check phi equivalence with circularity
0226   CHECK_SMALL(detail::radian_sym(cmp.template get<eBoundPhi>() -
0227                                  ref.template get<eBoundPhi>()),
0228               epsDir);
0229   CHECK_CLOSE_ABS(cmp.template get<eBoundTheta>(),
0230                   ref.template get<eBoundTheta>(), epsDir);
0231   CHECK_CLOSE_ABS(cmp.template get<eBoundQOverP>(),
0232                   ref.template get<eBoundQOverP>(), epsMom);
0233   // check derived parameters
0234   CHECK_CLOSE_ABS(cmp.position(geoCtx), ref.position(geoCtx), epsPos);
0235   CHECK_CLOSE_ABS(cmp.time(), ref.time(), epsTime);
0236   CHECK_CLOSE_ABS(cmp.direction(), ref.direction(), epsDir);
0237   CHECK_CLOSE_ABS(cmp.absoluteMomentum(), ref.absoluteMomentum(), epsMom);
0238   // charge should be identical not just similar
0239   BOOST_CHECK_EQUAL(cmp.charge(), ref.charge());
0240 }
0241 
0242 /// Check that two parameters covariances are consistent within the tolerances.
0243 ///
0244 /// \warning Does not check that the parameters value itself are consistent.
0245 inline void checkCovarianceConsistency(const Acts::BoundTrackParameters& cmp,
0246                                        const Acts::BoundTrackParameters& ref,
0247                                        double relativeTolerance) {
0248   // either both or none have covariance set
0249   if (cmp.covariance().has_value()) {
0250     // comparison parameters have covariance but the reference does not
0251     BOOST_CHECK(ref.covariance().has_value());
0252   }
0253   if (ref.covariance().has_value()) {
0254     // reference parameters have covariance but the comparison does not
0255     BOOST_CHECK(cmp.covariance().has_value());
0256   }
0257   if (cmp.covariance().has_value() && ref.covariance().has_value()) {
0258     CHECK_CLOSE_COVARIANCE(cmp.covariance().value(), ref.covariance().value(),
0259                            relativeTolerance);
0260   }
0261 }
0262 
0263 // helpers to construct target surfaces from track states
0264 
0265 /// Construct the transformation from the curvilinear to the global coordinates.
0266 inline Acts::Transform3 createCurvilinearTransform(
0267     const Acts::BoundTrackParameters& params,
0268     const Acts::GeometryContext& geoCtx) {
0269   using namespace Acts;
0270 
0271   Vector3 unitW = params.direction();
0272   auto [unitU, unitV] = createCurvilinearUnitVectors(unitW);
0273 
0274   RotationMatrix3 rotation = RotationMatrix3::Zero();
0275   rotation.col(0) = unitU;
0276   rotation.col(1) = unitV;
0277   rotation.col(2) = unitW;
0278   Transform3 toGlobal = makeTransform3(rotation, params.position(geoCtx));
0279 
0280   return toGlobal;
0281 }
0282 
0283 /// Construct a z-cylinder centered at zero with the track on its surface.
0284 struct ZCylinderSurfaceBuilder {
0285   std::shared_ptr<Acts::CylinderSurface> operator()(
0286       const Acts::BoundTrackParameters& params,
0287       const Acts::GeometryContext& geoCtx) {
0288     using namespace Acts;
0289 
0290     auto radius = params.position(geoCtx).template head<2>().norm();
0291     auto halfz = std::numeric_limits<double>::max();
0292     return Surface::makeShared<CylinderSurface>(Transform3::Identity(), radius,
0293                                                 halfz);
0294   }
0295 };
0296 
0297 /// Construct a disc at track position with plane normal along track tangent.
0298 struct DiscSurfaceBuilder {
0299   std::shared_ptr<Acts::DiscSurface> operator()(
0300       const Acts::BoundTrackParameters& params,
0301       const Acts::GeometryContext& geoCtx) {
0302     using namespace Acts;
0303     using namespace UnitLiterals;
0304 
0305     auto cl = createCurvilinearTransform(params, geoCtx);
0306     // shift the origin of the plane so the local particle position does not
0307     // sit directly at the rho=0,phi=undefined singularity
0308     // TODO this is a hack do avoid issues with the numerical covariance
0309     //      transport that does not work well at rho=0,
0310     Vector3 localOffset = Vector3::Zero();
0311     localOffset[ePos0] = 1_cm;
0312     localOffset[ePos1] = -1_cm;
0313     Vector3 globalOriginDelta = cl.linear() * localOffset;
0314     cl.pretranslate(globalOriginDelta);
0315 
0316     return Surface::makeShared<DiscSurface>(cl);
0317   }
0318 };
0319 
0320 /// Construct a plane at track position with plane normal along track tangent.
0321 struct PlaneSurfaceBuilder {
0322   std::shared_ptr<Acts::PlaneSurface> operator()(
0323       const Acts::BoundTrackParameters& params,
0324       const Acts::GeometryContext& geoCtx) {
0325     using namespace Acts;
0326 
0327     return Surface::makeShared<PlaneSurface>(
0328         createCurvilinearTransform(params, geoCtx));
0329   }
0330 };
0331 
0332 /// Construct a z-straw at the track position.
0333 struct ZStrawSurfaceBuilder {
0334   std::shared_ptr<Acts::StrawSurface> operator()(
0335       const Acts::BoundTrackParameters& params,
0336       const Acts::GeometryContext& geoCtx) {
0337     using namespace Acts;
0338 
0339     return Surface::makeShared<StrawSurface>(
0340         Transform3(Translation3(params.position(geoCtx))));
0341   }
0342 };
0343 
0344 // helper functions to run the propagation with additional checks
0345 
0346 /// Propagate the initial parameters for the given pathlength in space.
0347 ///
0348 /// Use a negative path length to indicate backward propagation.
0349 template <typename propagator_t,
0350           typename options_t = typename propagator_t::template Options<>>
0351 inline std::pair<Acts::BoundTrackParameters, double> transportFreely(
0352     const propagator_t& propagator, const Acts::GeometryContext& geoCtx,
0353     const Acts::MagneticFieldContext& magCtx,
0354     const Acts::BoundTrackParameters& initialParams, double pathLength) {
0355   using namespace Acts;
0356   using namespace UnitLiterals;
0357 
0358   // setup propagation options
0359   options_t options(geoCtx, magCtx);
0360   options.direction = Direction::fromScalar(pathLength);
0361   options.pathLimit = pathLength;
0362 
0363   auto result = propagator.propagate(initialParams, options);
0364   BOOST_CHECK(result.ok());
0365   BOOST_CHECK(result.value().endParameters);
0366 
0367   return {*result.value().endParameters, result.value().pathLength};
0368 }
0369 
0370 /// Propagate the initial parameters to the target surface.
0371 ///
0372 /// The target aborter type can be chosen with @p targetAborter, whose value is
0373 /// not used.
0374 template <typename propagator_t,
0375           typename options_t = typename propagator_t::template Options<>,
0376           typename target_aborter_t = Acts::SurfaceReached>
0377 inline std::pair<Acts::BoundTrackParameters, double> transportToSurface(
0378     const propagator_t& propagator, const Acts::GeometryContext& geoCtx,
0379     const Acts::MagneticFieldContext& magCtx,
0380     const Acts::BoundTrackParameters& initialParams,
0381     const Acts::Surface& targetSurface, double pathLimit,
0382     const target_aborter_t& targetAborter = {}) {
0383   static_cast<void>(targetAborter);
0384   using namespace Acts;
0385   using namespace UnitLiterals;
0386 
0387   // setup propagation options
0388   options_t options(geoCtx, magCtx);
0389   options.direction = Direction::Forward();
0390   options.pathLimit = pathLimit;
0391 
0392   auto result = propagator.template propagate<options_t, target_aborter_t>(
0393       initialParams, targetSurface, options);
0394   BOOST_CHECK(result.ok());
0395   BOOST_CHECK(result.value().endParameters);
0396 
0397   return {*result.value().endParameters, result.value().pathLength};
0398 }
0399 
0400 // self-consistency tests for a single propagator
0401 
0402 /// Propagate the initial parameters the given path length along its
0403 /// trajectory and then propagate the final parameters back. Verify that the
0404 /// propagated parameters match the initial ones.
0405 template <typename propagator_t,
0406           typename options_t = typename propagator_t::template Options<>>
0407 inline void runForwardBackwardTest(
0408     const propagator_t& propagator, const Acts::GeometryContext& geoCtx,
0409     const Acts::MagneticFieldContext& magCtx,
0410     const Acts::BoundTrackParameters& initialParams, double pathLength,
0411     double epsPos, double epsTime, double epsDir, double epsMom) {
0412   // propagate parameters Acts::Direction::Forward()
0413   auto [fwdParams, fwdPathLength] = transportFreely<propagator_t, options_t>(
0414       propagator, geoCtx, magCtx, initialParams, pathLength);
0415   CHECK_CLOSE_ABS(fwdPathLength, pathLength, epsPos);
0416   // propagate propagated parameters back again
0417   auto [bwdParams, bwdPathLength] = transportFreely<propagator_t, options_t>(
0418       propagator, geoCtx, magCtx, fwdParams, -pathLength);
0419   CHECK_CLOSE_ABS(bwdPathLength, -pathLength, epsPos);
0420   // check that initial and back-propagated parameters match
0421   checkParametersConsistency(initialParams, bwdParams, geoCtx, epsPos, epsTime,
0422                              epsDir, epsMom);
0423 }
0424 
0425 /// Propagate the initial parameters once for the given path length and
0426 /// use the propagated parameters to define a target surface. Propagate the
0427 /// initial parameters again to the target surface. Verify that the surface has
0428 /// been found and the parameters are consistent.
0429 template <typename propagator_t, typename surface_builder_t,
0430           typename options_t = typename propagator_t::template Options<>,
0431           typename target_aborter_t = Acts::SurfaceReached>
0432 inline void runToSurfaceTest(
0433     const propagator_t& propagator, const Acts::GeometryContext& geoCtx,
0434     const Acts::MagneticFieldContext& magCtx,
0435     const Acts::BoundTrackParameters& initialParams, double pathLength,
0436     surface_builder_t&& buildTargetSurface, double epsPos, double epsTime,
0437     double epsDir, double epsMom, const target_aborter_t& targetAborter = {}) {
0438   // free propagation for the given path length
0439   auto [freeParams, freePathLength] = transportFreely<propagator_t, options_t>(
0440       propagator, geoCtx, magCtx, initialParams, pathLength);
0441   CHECK_CLOSE_ABS(freePathLength, pathLength, epsPos);
0442   // build a target surface at the propagated position
0443   auto surface = buildTargetSurface(freeParams, geoCtx);
0444   BOOST_CHECK(surface);
0445 
0446   // bound propagation onto the target surface
0447   // increase path length limit to ensure the surface can be reached
0448   auto [surfParams, surfPathLength] =
0449       transportToSurface<propagator_t, options_t, target_aborter_t>(
0450           propagator, geoCtx, magCtx, initialParams, *surface, 1.5 * pathLength,
0451           targetAborter);
0452   CHECK_CLOSE_ABS(surfPathLength, pathLength, epsPos);
0453 
0454   // check that the to-surface propagation matches the defining free parameters
0455   CHECK_CLOSE_ABS(surfParams.position(geoCtx), freeParams.position(geoCtx),
0456                   epsPos);
0457   CHECK_CLOSE_ABS(surfParams.time(), freeParams.time(), epsTime);
0458   CHECK_CLOSE_ABS(surfParams.direction(), freeParams.direction(), epsDir);
0459   CHECK_CLOSE_ABS(surfParams.absoluteMomentum(), freeParams.absoluteMomentum(),
0460                   epsMom);
0461   CHECK_CLOSE_ABS(surfPathLength, freePathLength, epsPos);
0462 }
0463 
0464 // consistency tests between two propagators
0465 
0466 /// Propagate the initial parameters along their trajectory for the given path
0467 /// length using two different propagators and verify consistent output.
0468 template <typename cmp_propagator_t, typename ref_propagator_t>
0469 inline void runForwardComparisonTest(
0470     const cmp_propagator_t& cmpPropagator,
0471     const ref_propagator_t& refPropagator, const Acts::GeometryContext& geoCtx,
0472     const Acts::MagneticFieldContext& magCtx,
0473     const Acts::BoundTrackParameters& initialParams, double pathLength,
0474     double epsPos, double epsTime, double epsDir, double epsMom,
0475     double tolCov) {
0476   // propagate twice using the two different propagators
0477   auto [cmpParams, cmpPath] = transportFreely<cmp_propagator_t>(
0478       cmpPropagator, geoCtx, magCtx, initialParams, pathLength);
0479   auto [refParams, refPath] = transportFreely<ref_propagator_t>(
0480       refPropagator, geoCtx, magCtx, initialParams, pathLength);
0481   // check parameter comparison
0482   checkParametersConsistency(cmpParams, refParams, geoCtx, epsPos, epsTime,
0483                              epsDir, epsMom);
0484   checkCovarianceConsistency(cmpParams, refParams, tolCov);
0485   CHECK_CLOSE_ABS(cmpPath, pathLength, epsPos);
0486   CHECK_CLOSE_ABS(refPath, pathLength, epsPos);
0487   CHECK_CLOSE_ABS(cmpPath, refPath, epsPos);
0488 }
0489 
0490 /// Propagate the initial parameters along their trajectory for the given path
0491 /// length using the reference propagator. Use the propagated track parameters
0492 /// to define a target plane. Propagate the initial parameters using two
0493 /// different propagators and verify consistent output.
0494 template <typename cmp_propagator_t, typename ref_propagator_t,
0495           typename surface_builder_t,
0496           typename target_aborter_t = Acts::SurfaceReached>
0497 inline void runToSurfaceComparisonTest(
0498     const cmp_propagator_t& cmpPropagator,
0499     const ref_propagator_t& refPropagator, const Acts::GeometryContext& geoCtx,
0500     const Acts::MagneticFieldContext& magCtx,
0501     const Acts::BoundTrackParameters& initialParams, double pathLength,
0502     surface_builder_t&& buildTargetSurface, double epsPos, double epsTime,
0503     double epsDir, double epsMom, double tolCov,
0504     const target_aborter_t& targetAborter = {}) {
0505   // free propagation with the reference propagator for the given path length
0506   auto [freeParams, freePathLength] = transportFreely<ref_propagator_t>(
0507       refPropagator, geoCtx, magCtx, initialParams, pathLength);
0508   CHECK_CLOSE_ABS(freePathLength, pathLength, epsPos);
0509 
0510   // build a target surface at the propagated position
0511   auto surface = buildTargetSurface(freeParams, geoCtx);
0512   BOOST_CHECK(surface);
0513 
0514   // propagate twice to the surface using the two different propagators
0515   // increase path length limit to ensure the surface can be reached
0516   auto [cmpParams, cmpPath] =
0517       transportToSurface<cmp_propagator_t,
0518                          typename cmp_propagator_t::template Options<>,
0519                          target_aborter_t>(cmpPropagator, geoCtx, magCtx,
0520                                            initialParams, *surface,
0521                                            1.5 * pathLength, targetAborter);
0522   auto [refParams, refPath] =
0523       transportToSurface<ref_propagator_t,
0524                          typename ref_propagator_t::template Options<>,
0525                          target_aborter_t>(refPropagator, geoCtx, magCtx,
0526                                            initialParams, *surface,
0527                                            1.5 * pathLength, targetAborter);
0528   // check parameter comparison
0529   checkParametersConsistency(cmpParams, refParams, geoCtx, epsPos, epsTime,
0530                              epsDir, epsMom);
0531   checkCovarianceConsistency(cmpParams, refParams, tolCov);
0532   CHECK_CLOSE_ABS(cmpPath, pathLength, epsPos);
0533   CHECK_CLOSE_ABS(refPath, pathLength, epsPos);
0534   CHECK_CLOSE_ABS(cmpPath, refPath, epsPos);
0535 }
0536 
0537 template <typename propagator_t>
0538 inline void runDenseForwardTest(
0539     const propagator_t& propagator, const Acts::GeometryContext& geoCtx,
0540     const Acts::MagneticFieldContext& magCtx, double p, double q,
0541     const Acts::Surface& targetSurface, const Acts::Material& material,
0542     double thickness, const Acts::Logger& logger = Acts::getDummyLogger()) {
0543   using namespace Acts;
0544   using namespace UnitLiterals;
0545 
0546   const auto particleHypothesis = ParticleHypothesis::muon();
0547   const float mass = particleHypothesis.mass();
0548   const float absQ = std::abs(q);
0549   const double initialQOverP = particleHypothesis.qOverP(p, q);
0550   const auto initialParams = BoundTrackParameters::createCurvilinear(
0551       Vector4(-1.5_m, 0, 0, 0), Vector3::UnitX(), initialQOverP,
0552       BoundVector::Constant(1e-16).asDiagonal(), particleHypothesis);
0553 
0554   typename propagator_t::template Options<> options(geoCtx, magCtx);
0555   options.maxSteps = 10000;
0556   options.stepping.maxStepSize = 100_mm;
0557   options.stepping.dense.meanEnergyLoss = true;
0558   options.stepping.dense.momentumCutOff = 1_MeV;
0559 
0560   const auto result =
0561       propagator.propagate(initialParams, targetSurface, options);
0562 
0563   BOOST_CHECK(result.ok());
0564   CHECK_CLOSE_REL(3_m, result->pathLength, 1e-6);
0565   BOOST_CHECK(result->endParameters);
0566 
0567   BoundTrackParameters endParams = result->endParameters.value();
0568 
0569   BOOST_CHECK(endParams.covariance());
0570   CHECK_CLOSE_ABS(initialParams.position(geoCtx) + Acts::Vector3(3_m, 0, 0),
0571                   endParams.position(geoCtx), 1e-6);
0572   CHECK_CLOSE_ABS(initialParams.direction(), endParams.direction(), 1e-6);
0573 
0574   const auto& cov = endParams.covariance().value();
0575 
0576   const double endQOverP = endParams.qOverP();
0577   const double endP = endParams.absoluteMomentum();
0578   const double endEloss =
0579       Acts::fastHypot(p, mass) - Acts::fastHypot(endP, mass);
0580   const double endErrX = std::sqrt(cov(eBoundLoc0, eBoundLoc0));
0581   const double endErrY = std::sqrt(cov(eBoundLoc1, eBoundLoc1));
0582   const double endErrQOverP = std::sqrt(cov(eBoundQOverP, eBoundQOverP));
0583   const double endErrP =
0584       (absQ / std::pow(endParams.qOverP(), 2)) * endErrQOverP;
0585   const double endErrE = (p / Acts::fastHypot(p, mass)) * endErrP;
0586   const double endErrTheta = std::sqrt(cov(eBoundTheta, eBoundTheta));
0587   const double endErrPhi = std::sqrt(cov(eBoundPhi, eBoundPhi));
0588 
0589   ACTS_VERBOSE("input q/p = " << initialQOverP);
0590   ACTS_VERBOSE("input p = " << p);
0591   ACTS_VERBOSE("output q/p = " << endQOverP);
0592   ACTS_VERBOSE("output p = " << endP);
0593   ACTS_VERBOSE("output t = " << endParams.time());
0594   ACTS_VERBOSE("output eloss = " << endEloss);
0595   ACTS_VERBOSE("output err x = " << endErrX);
0596   ACTS_VERBOSE("output err y = " << endErrY);
0597   ACTS_VERBOSE("output err q/p = " << endErrQOverP);
0598   ACTS_VERBOSE("output err p = " << endErrP);
0599   ACTS_VERBOSE("output err E = " << endErrE);
0600   ACTS_VERBOSE("output err theta = " << endErrTheta);
0601   ACTS_VERBOSE("output err phi = " << endErrPhi);
0602 
0603   BOOST_CHECK_CLOSE(endErrX, endErrY, 1e-6);
0604   BOOST_CHECK_CLOSE(endErrTheta, endErrPhi, 1e-6);
0605 
0606   const float elossInitial = computeEnergyLossMean(
0607       MaterialSlab(material, thickness), particleHypothesis.absolutePdg(), mass,
0608       initialQOverP, absQ);
0609   const float elossFinal = computeEnergyLossMean(
0610       MaterialSlab(material, thickness), particleHypothesis.absolutePdg(), mass,
0611       endQOverP, absQ);
0612 
0613   ACTS_VERBOSE("ref eloss initial = " << elossInitial);
0614   ACTS_VERBOSE("ref eloss final = " << elossFinal);
0615 
0616   // energy loss will be smaller than elossInitial because of the increasing
0617   // radiation losses
0618   BOOST_CHECK_LT(endEloss, elossInitial);
0619   BOOST_CHECK_GT(endEloss, elossFinal);
0620 
0621   const float theta0Initial = computeMultipleScatteringTheta0(
0622       MaterialSlab(material, thickness), particleHypothesis.absolutePdg(), mass,
0623       initialQOverP, absQ);
0624   const float theta0Final = computeMultipleScatteringTheta0(
0625       MaterialSlab(material, thickness), particleHypothesis.absolutePdg(), mass,
0626       endQOverP, absQ);
0627 
0628   ACTS_VERBOSE("ref theta0 initial = " << theta0Initial);
0629   ACTS_VERBOSE("ref theta0 final = " << theta0Final);
0630 
0631   // angle errors will be larger than theta0Initial because of the energy loss
0632   BOOST_CHECK_GE(endErrTheta, theta0Initial);
0633   BOOST_CHECK_LT(endErrTheta, theta0Final);
0634   CHECK_CLOSE_REL(endErrTheta, 0.5 * (theta0Initial + theta0Final), 0.1);
0635 
0636   // estimates the positional uncertainty after crossing the material
0637   // block by integrating the angle variance over substeps
0638   const auto estimateVarX = [&](double qOverP, std::size_t substeps) {
0639     double previousTheta0 = 0;
0640     double varAngle = 0;
0641     double varPosition = 0;
0642     double covAnglePosition = 0;
0643 
0644     const auto step = [&](double substep, double theta0) {
0645       double deltaVarTheta = square(theta0) - square(previousTheta0);
0646       double deltaVarPos = varAngle * square(substep) +
0647                            2 * covAnglePosition * substep +
0648                            deltaVarTheta * (square(substep) / 3);
0649       double deltaCovAnglePosition =
0650           varAngle * substep + deltaVarTheta * substep / 2;
0651       previousTheta0 = theta0;
0652       varAngle += deltaVarTheta;
0653       varPosition += deltaVarPos;
0654       covAnglePosition += deltaCovAnglePosition;
0655     };
0656 
0657     // step through the material block
0658     for (std::size_t i = 0; i < substeps; ++i) {
0659       const float theta0 = computeMultipleScatteringTheta0(
0660           MaterialSlab(material, (i + 1) * thickness / substeps),
0661           particleHypothesis.absolutePdg(), mass, qOverP, absQ);
0662       step(thickness / substeps, theta0);
0663     }
0664 
0665     // step to the target surface
0666     step(1_m, previousTheta0);
0667 
0668     return varPosition;
0669   };
0670 
0671   // as a lower bound for sigma_x we use the initial theta0
0672   const double lowerBoundVarX = estimateVarX(initialQOverP, 10);
0673   // as an upper bound for sigma_x we use the final theta0
0674   const double upperBoundVarX = estimateVarX(endQOverP, 10);
0675 
0676   const double lowerBoundSigmaX = std::sqrt(lowerBoundVarX);
0677   const double upperBoundSigmaX = std::sqrt(upperBoundVarX);
0678 
0679   ACTS_VERBOSE("ref lower bound sigma x = " << lowerBoundSigmaX);
0680   ACTS_VERBOSE("ref upper bound sigma x = " << upperBoundSigmaX);
0681 
0682   BOOST_CHECK_GT(endErrX, lowerBoundSigmaX);
0683   BOOST_CHECK_LT(endErrX, upperBoundSigmaX);
0684   CHECK_CLOSE_REL(endErrX, 0.5 * (lowerBoundSigmaX + upperBoundSigmaX), 0.2);
0685 
0686   const float qOverPSigma = computeEnergyLossLandauSigmaQOverP(
0687       MaterialSlab(material, thickness), mass, initialQOverP, absQ);
0688 
0689   ACTS_VERBOSE("ref sigma q/p = " << qOverPSigma);
0690 
0691   CHECK_CLOSE_REL(endErrQOverP, qOverPSigma, 1e-4);
0692 }