Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-09-18 08:24:26

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 <boost/test/tools/old/interface.hpp>
0010 #include <boost/test/unit_test.hpp>
0011 
0012 #include "Acts/Definitions/Algebra.hpp"
0013 #include "Acts/Definitions/TrackParametrization.hpp"
0014 #include "Acts/Definitions/Units.hpp"
0015 #include "Acts/EventData/detail/TestSourceLink.hpp"
0016 #include "Acts/Geometry/GeometryContext.hpp"
0017 #include "Acts/Geometry/GeometryIdentifier.hpp"
0018 #include "Acts/Geometry/LayerCreator.hpp"
0019 #include "Acts/Geometry/TrackingGeometry.hpp"
0020 #include "Acts/MagneticField/ConstantBField.hpp"
0021 #include "Acts/MagneticField/MagneticFieldContext.hpp"
0022 #include "Acts/Propagator/EigenStepper.hpp"
0023 #include "Acts/Propagator/Navigator.hpp"
0024 #include "Acts/Propagator/Propagator.hpp"
0025 #include "Acts/Seeding/EstimateTrackParamsFromSeed.hpp"
0026 #include "Acts/Seeding/TrackParamsEstimationError.hpp"
0027 #include "Acts/Seeding/detail/CircleFit.hpp"
0028 #include "Acts/Surfaces/Surface.hpp"
0029 #include "Acts/Utilities/Logger.hpp"
0030 #include "ActsTests/CommonHelpers/CylindricalTrackingGeometry.hpp"
0031 #include "ActsTests/CommonHelpers/FloatComparisons.hpp"
0032 #include "ActsTests/CommonHelpers/MeasurementsCreator.hpp"
0033 
0034 #include <array>
0035 #include <cmath>
0036 #include <memory>
0037 #include <optional>
0038 #include <random>
0039 #include <set>
0040 #include <span>
0041 #include <utility>
0042 #include <vector>
0043 
0044 using namespace Acts;
0045 using namespace Acts::UnitLiterals;
0046 
0047 namespace ActsTests {
0048 
0049 using ConstantFieldStepper = EigenStepper<>;
0050 using ConstantFieldPropagator = Propagator<ConstantFieldStepper, Navigator>;
0051 
0052 const auto geoCtx = GeometryContext::dangerouslyDefaultConstruct();
0053 const MagneticFieldContext magCtx;
0054 
0055 // detector geometry
0056 CylindricalTrackingGeometry geometryStore(geoCtx);
0057 const auto geometry = geometryStore();
0058 
0059 // Two dimensional measurement with zero resolution
0060 const MeasurementResolutionMap resolutions = {
0061     {GeometryIdentifier(),
0062      MeasurementResolution{MeasurementType::eLoc01, {0, 0}}}};
0063 
0064 // Construct initial track parameters.
0065 BoundTrackParameters makeParameters(double phi, double theta, double p,
0066                                     double q) {
0067   // create covariance matrix from reasonable standard deviations
0068   BoundVector stddev;
0069   stddev[eBoundLoc0] = 100_um;
0070   stddev[eBoundLoc1] = 100_um;
0071   stddev[eBoundTime] = 25_ns;
0072   stddev[eBoundPhi] = 2_degree;
0073   stddev[eBoundTheta] = 2_degree;
0074   stddev[eBoundQOverP] = 1 / 100_GeV;
0075   BoundMatrix cov = stddev.cwiseProduct(stddev).asDiagonal();
0076   // Let the particle starts from the origin
0077   Vector4 mPos4(0., 0., 0., 0.);
0078   return BoundTrackParameters::createCurvilinear(
0079       mPos4, phi, theta, q / p, cov, ParticleHypothesis::pionLike(std::abs(q)));
0080 }
0081 
0082 std::default_random_engine rng(42);
0083 
0084 BOOST_AUTO_TEST_SUITE(SeedingSuite)
0085 
0086 BOOST_AUTO_TEST_CASE(trackparameters_estimation_test) {
0087   // Construct a propagator with the cylinderal geometry and a constant magnetic
0088   // field along z
0089   Navigator navigator({
0090       geometry,
0091       true,  // sensitive
0092       true,  // material
0093       false  // passive
0094   });
0095   const Vector3 bField(0, 0, 2._T);
0096   auto field = std::make_shared<ConstantBField>(bField);
0097   ConstantFieldStepper stepper(std::move(field));
0098 
0099   ConstantFieldPropagator propagator(std::move(stepper), std::move(navigator));
0100 
0101   std::array<double, 2> pArray = {0.5_GeV, 1.0_GeV};
0102   std::array<double, 3> phiArray = {20._degree, 0._degree - 20._degree};
0103   std::array<double, 3> thetaArray = {80._degree, 90.0_degree, 100._degree};
0104   std::array<double, 2> qArray = {1, -1};
0105 
0106   auto logger = getDefaultLogger("estimateTrackParamsFromSeed", Logging::INFO);
0107 
0108   for (const auto& p : pArray) {
0109     for (const auto& phi : phiArray) {
0110       for (const auto& theta : thetaArray) {
0111         for (const auto& q : qArray) {
0112           BOOST_TEST_INFO("Test track with p = " << p << ", phi = " << phi
0113                                                  << ", theta = " << theta
0114                                                  << ", q = " << q);
0115           auto start = makeParameters(phi, theta, p, q);
0116           auto measurements = createMeasurements(propagator, geoCtx, magCtx,
0117                                                  start, resolutions, rng);
0118 
0119           // Create space points from different detector layers
0120           std::vector<Vector3> spacePoints;
0121           std::set<GeometryIdentifier::Value> usedLayers;
0122           const Surface* bottomSurface = nullptr;
0123           for (const auto& sl : measurements.sourceLinks) {
0124             const auto geoId = sl.m_geometryId;
0125             const auto& layer = geoId.layer();
0126             // Avoid to use space point from the same layers
0127             if (const auto it = usedLayers.find(layer);
0128                 it != usedLayers.end()) {
0129               continue;
0130             }
0131             const auto surface = geometry->findSurface(geoId);
0132             const auto& localPos = sl.parameters;
0133             const Vector3 globalFakeMom(1, 1, 1);
0134             const Vector3 globalPos =
0135                 surface->localToGlobal(geoCtx, localPos, globalFakeMom);
0136             spacePoints.push_back(globalPos);
0137             usedLayers.insert(layer);
0138             if (spacePoints.size() == 1) {
0139               bottomSurface = surface;
0140             }
0141           }
0142 
0143           // Check if there are at least 3 space points
0144           if (spacePoints.size() < 3) {
0145             BOOST_TEST_WARN("Number of space points less than 3.");
0146             continue;
0147           }
0148 
0149           // The truth track parameters at the bottom space point
0150           const auto& expBoundParams = measurements.truthParameters[0];
0151           BOOST_TEST_INFO(
0152               "The truth track parameters at the bottom space point: \n"
0153               << expBoundParams.transpose());
0154 
0155           // Test the free track parameters estimator
0156           FreeVector estFreeParams = estimateTrackParamsFromSeed(
0157               spacePoints[0], 0, spacePoints[1], spacePoints[2], bField);
0158           BOOST_CHECK(!estFreeParams.hasNaN());
0159 
0160           // Test the bound track parameters estimator
0161           auto estBoundParamsResult = estimateTrackParamsFromSeed(
0162               geoCtx, *bottomSurface, spacePoints[0], 0, spacePoints[1],
0163               spacePoints[2], bField);
0164           BOOST_CHECK(estBoundParamsResult.ok());
0165           const auto& estBoundParams = estBoundParamsResult.value();
0166           BOOST_TEST_INFO(
0167               "The estimated full track parameters at the bottom space point: "
0168               "\n"
0169               << estBoundParams.transpose());
0170 
0171           CHECK_CLOSE_ABS(estBoundParams[eBoundLoc0],
0172                           expBoundParams[eBoundLoc0], 1e-5);
0173           CHECK_CLOSE_ABS(estBoundParams[eBoundLoc1],
0174                           expBoundParams[eBoundLoc1], 1e-5);
0175           // @todo Understand why the estimated phi has a limited precision
0176           CHECK_CLOSE_ABS(estBoundParams[eBoundPhi], expBoundParams[eBoundPhi],
0177                           1e-1);
0178           CHECK_CLOSE_ABS(estBoundParams[eBoundTheta],
0179                           expBoundParams[eBoundTheta], 1e-2);
0180           CHECK_CLOSE_ABS(estBoundParams[eBoundQOverP],
0181                           expBoundParams[eBoundQOverP], 1e-2);
0182           // time is not estimated so we check if it is default zero
0183           CHECK_CLOSE_ABS(estBoundParams[eBoundTime], 0, 1e-6);
0184         }
0185       }
0186     }
0187   }
0188 }
0189 
0190 BOOST_AUTO_TEST_CASE(trackparm_estimate_aligined) {
0191   Vector3 sp0{-72.775, -0.325, -615.6};
0192   Vector3 sp1{-84.325, -0.325, -715.6};
0193   Vector3 sp2{-98.175, -0.325, -835.6};
0194   Vector3 bField{0, 0, 0.000899377};
0195 
0196   FreeVector params = estimateTrackParamsFromSeed(sp0, 0, sp1, sp2, bField);
0197   BOOST_CHECK_EQUAL(params[eFreeQOverP], 0);
0198 }
0199 
0200 BOOST_AUTO_TEST_CASE(trackparm_estimate_degenerate) {
0201   const Vector3 bField{0, 0, 2_T};
0202 
0203   // bottom and middle share a transverse position, so the estimation frame has
0204   // no x axis
0205   {
0206     const Vector3 sp0{10, 0, 0};
0207     const Vector3 sp1{10, 0, 100};
0208     const Vector3 sp2{10, 0, 200};
0209 
0210     BOOST_CHECK(
0211         !estimateTrackParamsFromSeed(sp0, 0, sp1, sp2, bField).allFinite());
0212   }
0213 
0214   // middle and top share a transverse position, so the conformal mapping maps
0215   // them onto each other
0216   {
0217     const Vector3 sp0{10, 0, 0};
0218     const Vector3 sp1{20, 5, 0};
0219     const Vector3 sp2{20, 5, 50};
0220 
0221     BOOST_CHECK(
0222         !estimateTrackParamsFromSeed(sp0, 0, sp1, sp2, bField).allFinite());
0223   }
0224 }
0225 
0226 // Multi space point estimator against truth, and against the closed-form
0227 // three-point estimator at N=3.
0228 BOOST_AUTO_TEST_CASE(spacepoint_estimator_vs_truth) {
0229   Navigator navigator({
0230       geometry,
0231       true,   // sensitive
0232       true,   // material
0233       false,  // passive
0234   });
0235   const Vector3 bField(0, 0, 2._T);
0236   auto field = std::make_shared<ConstantBField>(bField);
0237   ConstantFieldStepper stepper(std::move(field));
0238   ConstantFieldPropagator propagator(std::move(stepper), std::move(navigator));
0239 
0240   std::array<double, 2> pArray = {0.5_GeV, 1.0_GeV};
0241   std::array<double, 3> phiArray = {20._degree, 0._degree, -20._degree};
0242   std::array<double, 3> thetaArray = {80._degree, 90._degree, 100._degree};
0243   std::array<double, 2> qArray = {1, -1};
0244 
0245   for (const auto& p : pArray) {
0246     for (const auto& phi : phiArray) {
0247       for (const auto& theta : thetaArray) {
0248         for (const auto& q : qArray) {
0249           BOOST_TEST_INFO("Test track with p = " << p << ", phi = " << phi
0250                                                  << ", theta = " << theta
0251                                                  << ", q = " << q);
0252           auto start = makeParameters(phi, theta, p, q);
0253           auto measurements = createMeasurements(propagator, geoCtx, magCtx,
0254                                                  start, resolutions, rng);
0255 
0256           // Collect one space point per detector layer, in track order.
0257           std::vector<Vector3> spacePoints;
0258           std::set<GeometryIdentifier::Value> usedLayers;
0259           const Surface* bottomSurface = nullptr;
0260           for (const auto& sl : measurements.sourceLinks) {
0261             const auto geoId = sl.m_geometryId;
0262             const auto& layer = geoId.layer();
0263             if (const auto it = usedLayers.find(layer);
0264                 it != usedLayers.end()) {
0265               continue;
0266             }
0267             const auto surface = geometry->findSurface(geoId);
0268             const Vector3 globalFakeMom(1, 1, 1);
0269             const Vector3 globalPos =
0270                 surface->localToGlobal(geoCtx, sl.parameters, globalFakeMom);
0271             spacePoints.push_back(globalPos);
0272             usedLayers.insert(layer);
0273             if (spacePoints.size() == 1) {
0274               bottomSurface = surface;
0275             }
0276           }
0277 
0278           if (spacePoints.size() < 3) {
0279             BOOST_TEST_WARN("Number of space points less than 3.");
0280             continue;
0281           }
0282 
0283           const auto& expBoundParams = measurements.truthParameters[0];
0284 
0285           auto freeResult =
0286               estimateTrackParamsFromSpacePoints(spacePoints, bField);
0287           BOOST_REQUIRE(freeResult.ok());
0288           auto boundResult = transformFreeToBoundParameters(
0289               *freeResult, *bottomSurface, geoCtx);
0290           BOOST_REQUIRE(boundResult.ok());
0291           const auto& estBoundParams = boundResult.value();
0292 
0293           CHECK_CLOSE_ABS(estBoundParams[eBoundLoc0],
0294                           expBoundParams[eBoundLoc0], 1e-4);
0295           CHECK_CLOSE_ABS(estBoundParams[eBoundLoc1],
0296                           expBoundParams[eBoundLoc1], 1e-4);
0297           CHECK_CLOSE_ABS(estBoundParams[eBoundPhi], expBoundParams[eBoundPhi],
0298                           1e-1);
0299           CHECK_CLOSE_ABS(estBoundParams[eBoundTheta],
0300                           expBoundParams[eBoundTheta], 1e-2);
0301           CHECK_CLOSE_ABS(estBoundParams[eBoundQOverP],
0302                           expBoundParams[eBoundQOverP], 1e-2);
0303 
0304           // At N=3 both estimators must agree, which pins the sign and
0305           // momentum conventions.
0306           auto free3 = estimateTrackParamsFromSpacePoints(
0307               std::span<const Vector3>(spacePoints).first(3), bField, 0);
0308           BOOST_CHECK(free3.ok());
0309           const FreeVector ref = estimateTrackParamsFromSeed(
0310               spacePoints[0], 0, spacePoints[1], spacePoints[2], bField);
0311           for (int i = 0; i < 3; ++i) {
0312             CHECK_CLOSE_ABS((*free3)[eFreeDir0 + i], ref[eFreeDir0 + i], 1e-6);
0313           }
0314           CHECK_CLOSE_ABS((*free3)[eFreeQOverP], ref[eFreeQOverP],
0315                           1e-6 * std::abs(ref[eFreeQOverP]) + 1e-9);
0316         }
0317       }
0318     }
0319   }
0320 }
0321 
0322 // More than half a turn, to exercise the phi unwrapping and the R-Z fit.
0323 BOOST_AUTO_TEST_CASE(spacepoint_estimator_large_arc) {
0324   const double bMag = 2._T;
0325   const Vector3 bField(0, 0, bMag);
0326   const double radius = 300;  // mm
0327   const double theta = 70._degree;
0328   const double lambda = 1 / std::tan(theta);  // dz/ds
0329   const double alpha0 = 0.3;
0330   const double dAlpha = 40._degree;  // 6 points -> 200 degrees
0331   const std::size_t nsp = 6;
0332 
0333   auto runWith = [&](std::size_t refineIter) {
0334     std::vector<Vector3> spacePoints;
0335     for (std::size_t i = 0; i < nsp; ++i) {
0336       const double a = alpha0 + i * dAlpha;
0337       const double s = radius * (a - alpha0);
0338       spacePoints.emplace_back(radius * std::cos(a), radius * std::sin(a),
0339                                100 + lambda * s);
0340     }
0341     const Vector3 first = spacePoints.front();
0342 
0343     auto result =
0344         estimateTrackParamsFromSpacePoints(spacePoints, bField, 0, refineIter);
0345     BOOST_CHECK(result.ok());
0346     const FreeVector& fp = *result;
0347     BOOST_CHECK(!fp.hasNaN());
0348 
0349     for (int i = 0; i < 3; ++i) {
0350       CHECK_CLOSE_ABS(fp[eFreePos0 + i], first[i], 1e-6);
0351     }
0352 
0353     // Tangent at the first point: transverse (-sin a0, cos a0), z slope lambda.
0354     Vector3 dir(-std::sin(alpha0), std::cos(alpha0), lambda);
0355     dir.normalize();
0356     for (int i = 0; i < 3; ++i) {
0357       CHECK_CLOSE_ABS(fp[eFreeDir0 + i], dir[i], 1e-6);
0358     }
0359 
0360     // |q/p| = sin(theta) / (R * |B|).
0361     const double expAbsQOverP = std::sin(theta) / (radius * bMag);
0362     CHECK_CLOSE_ABS(std::abs(fp[eFreeQOverP]), expAbsQOverP,
0363                     1e-6 * expAbsQOverP);
0364   };
0365 
0366   runWith(3);
0367   runWith(0);
0368 }
0369 
0370 // The reference point can be any space point: the fitted helix is the same and
0371 // only the point it is evaluated at moves.
0372 BOOST_AUTO_TEST_CASE(spacepoint_estimator_reference_index) {
0373   const double bMag = 2._T;
0374   const Vector3 bField(0, 0, bMag);
0375   const double radius = 300;  // mm
0376   const double theta = 70._degree;
0377   const double lambda = 1 / std::tan(theta);  // dz/ds
0378   const double alpha0 = 0.3;
0379   const double dAlpha = 40._degree;  // 6 points -> 200 degrees
0380   const std::size_t nsp = 6;
0381 
0382   std::vector<Vector3> spacePoints;
0383   for (std::size_t i = 0; i < nsp; ++i) {
0384     const double a = alpha0 + i * dAlpha;
0385     const double s = radius * (a - alpha0);
0386     spacePoints.emplace_back(radius * std::cos(a), radius * std::sin(a),
0387                              100 + lambda * s);
0388   }
0389 
0390   const auto atFirst = estimateTrackParamsFromSpacePoints(spacePoints, bField);
0391   BOOST_REQUIRE(atFirst.ok());
0392 
0393   for (std::size_t ref = 0; ref < nsp; ++ref) {
0394     BOOST_TEST_CONTEXT("referenceIndex = " << ref) {
0395       auto result = estimateTrackParamsFromSpacePoints(spacePoints, bField, 0,
0396                                                        0, {}, ref);
0397       BOOST_REQUIRE(result.ok());
0398       const FreeVector& fp = *result;
0399 
0400       // The parameters sit on the chosen space point.
0401       for (int i = 0; i < 3; ++i) {
0402         CHECK_CLOSE_ABS(fp[eFreePos0 + i], spacePoints[ref][i], 1e-6);
0403       }
0404 
0405       // Tangent of the truth helix there: transverse (-sin a, cos a), z slope
0406       // lambda.
0407       const double a = alpha0 + ref * dAlpha;
0408       Vector3 dir(-std::sin(a), std::cos(a), lambda);
0409       dir.normalize();
0410       for (int i = 0; i < 3; ++i) {
0411         CHECK_CLOSE_ABS(fp[eFreeDir0 + i], dir[i], 1e-6);
0412       }
0413 
0414       // The helix itself does not depend on the reference, so charge and
0415       // momentum are identical to the estimate at the first point.
0416       BOOST_CHECK_EQUAL(fp[eFreeQOverP], (*atFirst)[eFreeQOverP]);
0417     }
0418   }
0419 
0420   // The default is the first space point.
0421   const auto atZero =
0422       estimateTrackParamsFromSpacePoints(spacePoints, bField, 0, 0, {}, 0);
0423   BOOST_REQUIRE(atZero.ok());
0424   BOOST_CHECK_EQUAL(*atZero, *atFirst);
0425 }
0426 
0427 // Zero field: a straight-line fit with vanishing q/p.
0428 BOOST_AUTO_TEST_CASE(spacepoint_estimator_zero_field) {
0429   const Vector3 bField = Vector3::Zero();
0430   const Vector3 dir = Vector3(1, 2, 3).normalized();
0431   const Vector3 p0(10, -5, 7);
0432   std::vector<Vector3> spacePoints;
0433   for (int i = 0; i < 5; ++i) {
0434     spacePoints.push_back(p0 + i * 20 * dir);
0435   }
0436 
0437   auto result = estimateTrackParamsFromSpacePoints(spacePoints, bField, 0);
0438   BOOST_CHECK(result.ok());
0439   const FreeVector& fp = *result;
0440   BOOST_CHECK_EQUAL(fp[eFreeQOverP], 0);
0441   for (int i = 0; i < 3; ++i) {
0442     CHECK_CLOSE_ABS(fp[eFreeDir0 + i], dir[i], 1e-9);
0443     CHECK_CLOSE_ABS(fp[eFreePos0 + i], p0[i], 1e-9);
0444   }
0445 }
0446 
0447 // Fewer than three space points is an error.
0448 BOOST_AUTO_TEST_CASE(spacepoint_estimator_not_enough_points) {
0449   const std::array<Vector3, 2> spacePoints{Vector3(1, 0, 0), Vector3(2, 0, 0)};
0450   auto result =
0451       estimateTrackParamsFromSpacePoints(spacePoints, Vector3(0, 0, 2._T), 0);
0452   BOOST_CHECK(!result.ok());
0453   BOOST_CHECK(result.error() ==
0454               TrackParamsEstimationError::NotEnoughSpacePoints);
0455 }
0456 
0457 // Coincident space points must report a degenerate fit.
0458 BOOST_AUTO_TEST_CASE(spacepoint_estimator_degenerate) {
0459   const std::array<Vector3, 4> spacePoints{Vector3(3, 4, 5), Vector3(3, 4, 5),
0460                                            Vector3(3, 4, 5), Vector3(3, 4, 5)};
0461   auto result =
0462       estimateTrackParamsFromSpacePoints(spacePoints, Vector3(0, 0, 2._T), 0);
0463   BOOST_CHECK(!result.ok());
0464   BOOST_CHECK(result.error() == TrackParamsEstimationError::DegenerateFit);
0465 }
0466 
0467 // The Taubin fit recovers a known circle and rejects collinear input.
0468 BOOST_AUTO_TEST_CASE(taubin_circle_fit) {
0469   const Vector2 center(3.5, -2);
0470   const double radius = 12;
0471   std::vector<Vector3> points;
0472   for (int i = 0; i < 7; ++i) {
0473     const double a = 0.2 + i * 0.35;
0474     const Vector2 xy = center + radius * Vector2(std::cos(a), std::sin(a));
0475     points.emplace_back(xy.x(), xy.y(), 0);
0476   }
0477   const auto circle = Acts::detail::fitCircleTaubin(points);
0478   BOOST_REQUIRE(circle.has_value());
0479   CHECK_CLOSE_ABS(circle->center.x(), center.x(), 1e-6);
0480   CHECK_CLOSE_ABS(circle->center.y(), center.y(), 1e-6);
0481   CHECK_CLOSE_ABS(circle->radius, radius, 1e-6);
0482 
0483   // Collinear points do not define a finite circle.
0484   std::vector<Vector3> line;
0485   for (int i = 0; i < 5; ++i) {
0486     line.emplace_back(2 * i, 1 + 6 * i, 0);
0487   }
0488   BOOST_CHECK(!Acts::detail::fitCircleTaubin(line).has_value());
0489 }
0490 
0491 // A near-zero weight on a displaced point recovers the true circle.
0492 BOOST_AUTO_TEST_CASE(taubin_circle_fit_weighted) {
0493   const Vector2 center(3.5, -2);
0494   const double radius = 12;
0495   std::vector<Vector3> points;
0496   std::vector<double> weights;
0497   for (int i = 0; i < 7; ++i) {
0498     const double a = 0.2 + i * 0.35;
0499     const Vector2 xy = center + radius * Vector2(std::cos(a), std::sin(a));
0500     points.emplace_back(xy.x(), xy.y(), 0);
0501     weights.push_back(1);
0502   }
0503   // Displace the last point radially outward and give it a tiny weight.
0504   const double aOut = 0.2 + 6 * 0.35;
0505   const Vector2 xyOut =
0506       center + (radius + 5) * Vector2(std::cos(aOut), std::sin(aOut));
0507   points.back() = Vector3(xyOut.x(), xyOut.y(), 0);
0508   weights.back() = 1e-6;
0509 
0510   const auto unweighted = Acts::detail::fitCircleTaubin(points);
0511   BOOST_REQUIRE(unweighted.has_value());
0512   BOOST_CHECK(std::abs(unweighted->radius - radius) > 0.1);
0513 
0514   const auto weighted = Acts::detail::fitCircleTaubin(points, weights);
0515   BOOST_REQUIRE(weighted.has_value());
0516   CHECK_CLOSE_ABS(weighted->center.x(), center.x(), 1e-3);
0517   CHECK_CLOSE_ABS(weighted->center.y(), center.y(), 1e-3);
0518   CHECK_CLOSE_ABS(weighted->radius, radius, 1e-3);
0519 }
0520 
0521 // The same through the estimator: a helix with one point displaced in z, where
0522 // uniform weights bias theta and a small weight does not.
0523 BOOST_AUTO_TEST_CASE(spacepoint_estimator_weighted) {
0524   const double bMag = 2._T;
0525   const Vector3 bField(0, 0, bMag);
0526   const double radius = 300;  // mm
0527   const double theta = 70._degree;
0528   const double lambda = 1 / std::tan(theta);  // dz/ds
0529   const double alpha0 = 0.3;
0530   const double dAlpha = 40._degree;
0531   const std::size_t nsp = 6;
0532 
0533   std::vector<Vector3> spacePoints;
0534   for (std::size_t i = 0; i < nsp; ++i) {
0535     const double a = alpha0 + i * dAlpha;
0536     const double s = radius * (a - alpha0);
0537     spacePoints.emplace_back(radius * std::cos(a), radius * std::sin(a),
0538                              100 + lambda * s);
0539   }
0540   // Displace one point in z only (transverse position untouched, so the circle
0541   // fit stays on the true circle) and down-weight it.
0542   spacePoints[3].z() += 50;
0543   std::vector<double> weights(nsp, 1);
0544   weights[3] = 1e-6;
0545 
0546   const std::span<const Vector3> sp(spacePoints);
0547 
0548   auto biased = estimateTrackParamsFromSpacePoints(sp, bField, 0, 0);
0549   BOOST_CHECK(biased.ok());
0550   auto corrected =
0551       estimateTrackParamsFromSpacePoints(sp, bField, 0, 0, weights);
0552   BOOST_CHECK(corrected.ok());
0553 
0554   Vector3 dir(-std::sin(alpha0), std::cos(alpha0), lambda);
0555   dir.normalize();
0556 
0557   // The weighted fit recovers the true direction; the unweighted one is pulled
0558   // off by the displaced point.
0559   double biasedErr = 0, correctedErr = 0;
0560   for (int i = 0; i < 3; ++i) {
0561     biasedErr += std::abs((*biased)[eFreeDir0 + i] - dir[i]);
0562     correctedErr += std::abs((*corrected)[eFreeDir0 + i] - dir[i]);
0563   }
0564   BOOST_CHECK(correctedErr < biasedErr);
0565   CHECK_CLOSE_ABS((*corrected)[eFreeDir0 + 2], dir[2], 1e-3);
0566 }
0567 
0568 BOOST_AUTO_TEST_SUITE_END()
0569 
0570 }  // namespace ActsTests