Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2025-10-13 08:19:11

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/unit_test.hpp>
0010 
0011 #include "Acts/Definitions/Units.hpp"
0012 #include "Acts/Seeding/CompositeSpacePointLineFitter.hpp"
0013 #include "Acts/Seeding/CompositeSpacePointLineSeeder.hpp"
0014 #include "Acts/Utilities/StringHelpers.hpp"
0015 #include "Acts/Utilities/UnitVectors.hpp"
0016 #include "Acts/Utilities/VectorHelpers.hpp"
0017 
0018 #include <chrono>
0019 #include <format>
0020 #include <random>
0021 #include <ranges>
0022 #include <set>
0023 #include <span>
0024 
0025 #include <TFile.h>
0026 #include <TTree.h>
0027 
0028 using namespace Acts;
0029 using namespace Acts::Experimental;
0030 using namespace Acts::Experimental::detail;
0031 using namespace Acts::UnitLiterals;
0032 using namespace Acts::detail::LineHelper;
0033 using namespace Acts::PlanarHelper;
0034 using namespace Acts::VectorHelpers;
0035 
0036 using TimePoint_t = std::chrono::system_clock::time_point;
0037 using RandomEngine = std::mt19937;
0038 using uniform = std::uniform_real_distribution<double>;
0039 using normal_t = std::normal_distribution<double>;
0040 using Line_t = CompSpacePointAuxiliaries::Line_t;
0041 using ResidualIdx = CompSpacePointAuxiliaries::ResidualIdx;
0042 using FitParIndex = CompSpacePointAuxiliaries::FitParIndex;
0043 using ParamVec_t = CompositeSpacePointLineFitter::ParamVec_t;
0044 using Fitter_t = CompositeSpacePointLineFitter;
0045 
0046 constexpr auto logLvl = Acts::Logging::Level::INFO;
0047 constexpr std::size_t nEvents = 1;
0048 
0049 ACTS_LOCAL_LOGGER(getDefaultLogger("StrawLineFitterTest", logLvl));
0050 
0051 namespace ActsTests {
0052 
0053 class FitTestSpacePoint {
0054  public:
0055   /// @brief Constructor for straw wires
0056   /// @param pos: Position of the wire
0057   /// @param driftR: Straw drift radius
0058   /// @param driftRUncert: Uncertainty on the drift radius uncertainty
0059   /// @param twinUncert: Uncertainty on the measurement along the straw
0060   FitTestSpacePoint(const Vector3& pos, const double driftR,
0061                     const double driftRUncert,
0062                     const std::optional<double> twnUncert = std::nullopt)
0063       : m_position{pos},
0064         m_driftR{driftR},
0065         m_measLoc0{twnUncert != std::nullopt} {
0066     using enum ResidualIdx;
0067     m_covariance[toUnderlying(bending)] = Acts::square(driftRUncert);
0068     m_covariance[toUnderlying(nonBending)] =
0069         Acts::square(twnUncert.value_or(0.));
0070   }
0071 
0072   /// @brief Constructor for strip measurements
0073   FitTestSpacePoint(const Vector3& stripPos, const Vector3& stripDir,
0074                     const Vector3& toNext, const double uncertLoc0,
0075                     const double uncertLoc1)
0076       : m_position{stripPos},
0077         m_sensorDir{stripDir},
0078         m_toNextSen{toNext},
0079         m_measLoc0{uncertLoc0 > 0.},
0080         m_measLoc1{uncertLoc1 > 0.} {
0081     using enum ResidualIdx;
0082     m_covariance[toUnderlying(nonBending)] = Acts::square(uncertLoc0);
0083     m_covariance[toUnderlying(bending)] = Acts::square(uncertLoc1);
0084   }
0085   /// @brief Position of the space point
0086   const Vector3& localPosition() const { return m_position; }
0087   /// @brief Wire direction
0088   const Vector3& sensorDirection() const { return m_sensorDir; }
0089   /// @brief To next sensor in the plane
0090   const Vector3& toNextSensor() const { return m_toNextSen; }
0091   /// @brief To next straw layer
0092   const Vector3& planeNormal() const { return m_planeNorm; }
0093   /// @brief Measurement's radius
0094   double driftRadius() const { return m_driftR.value_or(0.); }
0095   /// @brief Measurement's covariance
0096   const std::array<double, 3>& covariance() const { return m_covariance; }
0097   /// @brief Time of record
0098   double time() const { return m_time.value_or(0.); }
0099   /// @brief All measurements are straws
0100   bool isStraw() const { return m_driftR.has_value(); }
0101   /// @brief Check whether the space point has a time value
0102   bool hasTime() const { return m_time.has_value(); }
0103   /// @brief Check whether the space point measures the non-bending direction
0104   bool measuresLoc0() const { return m_measLoc0; }
0105   /// @brief Check whether the space point measures the bending direction
0106   bool measuresLoc1() const { return m_measLoc1 || isStraw(); }
0107   /// @brief Sets the straw tube's drift radius
0108   void updateDriftR(const double updatedR) { m_driftR = updatedR; }
0109   /// @brief Updates the position of the space point
0110   void updatePosition(const Vector3& newPos) { m_position = newPos; }
0111 
0112  private:
0113   Vector3 m_position{Vector3::Zero()};
0114   Vector3 m_sensorDir{Vector3::UnitX()};
0115   Vector3 m_toNextSen{Vector3::UnitY()};
0116   Vector3 m_planeNorm{m_sensorDir.cross(m_toNextSen).normalized()};
0117   std::optional<double> m_driftR{std::nullopt};
0118   std::optional<double> m_time{std::nullopt};
0119   std::array<double, 3> m_covariance{filledArray<double, 3>(0)};
0120   bool m_measLoc0{false};
0121   bool m_measLoc1{false};
0122 };
0123 
0124 static_assert(CompositeSpacePoint<FitTestSpacePoint>);
0125 
0126 using Container_t = std::vector<std::shared_ptr<FitTestSpacePoint>>;
0127 
0128 static_assert(CompositeSpacePointContainer<Container_t>);
0129 class SpCalibrator {
0130  public:
0131   static double driftUncert(const double r) {
0132     return 0.2_mm / (1._mm + Acts::square(r)) + 0.1_mm;
0133   }
0134   /// @brief Calibrate a set of straw measurements using the best known estimate on a straight line track
0135   /// @param ctx: Calibration context (Needed by conept interface)
0136   /// @param trackPos: Position of the track at z=0.
0137   /// @param trackDir: Direction of the track in the local frame
0138   /// @param timeOffSet: Offset in the time of arrival (To be implemented)
0139   /// @param uncalibCont: Uncalibrated composite space point container
0140   Container_t calibrate(const Acts::CalibrationContext& /*ctx*/,
0141                         const Vector3& trackPos, const Vector3& trackDir,
0142                         const double /*timeOffSet*/,
0143                         const Container_t& uncalibCont) const {
0144     Container_t calibMeas{};
0145     for (const auto& sp : uncalibCont) {
0146       if (!sp->measuresLoc0() || !sp->measuresLoc1()) {
0147         /// Estimate the best position along the sensor
0148         auto bestPos = lineIntersect(trackPos, trackDir, sp->localPosition(),
0149                                      sp->sensorDirection());
0150         sp->updatePosition(bestPos.position());
0151       }
0152       if (sp->isStraw()) {
0153         sp->updateDriftR(Acts::abs(sp->driftRadius()));
0154       }
0155     }
0156     return uncalibCont;
0157   }
0158   /// @brief Updates the sign of the Straw's drift radii indicating that they are on the left (-1)
0159   ///        or right side (+1) of the track line
0160   void updateSigns(const Vector3& trackPos, const Vector3& trackDir,
0161                    Container_t& measurements) const {
0162     auto signs =
0163         CompSpacePointAuxiliaries::strawSigns(trackPos, trackDir, measurements);
0164     /// The signs have the same size as the measurement container
0165     for (std::size_t s = 0; s < signs.size(); ++s) {
0166       /// Take care to not turn strips into straws by accident
0167       if (measurements[s]->isStraw()) {
0168         measurements[s]->updateDriftR(
0169             Acts::abs(measurements[s]->driftRadius()) * signs[s]);
0170       }
0171     }
0172   }
0173 };
0174 /// Ensure that the Test space point calibrator satisfies the calibrator concept
0175 static_assert(
0176     CompositeSpacePointCalibrator<SpCalibrator, Container_t, Container_t>);
0177 
0178 /// @brief Generates a random straight line
0179 /// @param engine Random number sequence to draw the parameters from
0180 /// @return A Line object instantiated with the generated parameters
0181 Line_t generateLine(RandomEngine& engine) {
0182   using ParIndex = Line_t::ParIndex;
0183   Line_t::ParamVector linePars{};
0184   linePars[toUnderlying(ParIndex::phi)] =
0185       std::uniform_real_distribution{-120_degree, 120_degree}(engine);
0186   linePars[toUnderlying(ParIndex::x0)] =
0187       std::uniform_real_distribution{-5000., 5000.}(engine);
0188   linePars[toUnderlying(ParIndex::y0)] =
0189       std::uniform_real_distribution{-5000., 5000.}(engine);
0190   linePars[toUnderlying(ParIndex::theta)] =
0191       std::uniform_real_distribution{5_degree, 175_degree}(engine);
0192   if (Acts::abs(linePars[toUnderlying(ParIndex::theta)] - 90._degree) <
0193       0.2_degree) {
0194     return generateLine(engine);
0195   }
0196   Line_t line{linePars};
0197 
0198   ACTS_DEBUG(
0199       "\n\n\n"
0200       << __func__ << "() " << __LINE__ << " - Generated parameters "
0201       << std::format("theta: {:.2f}, phi: {:.2f}, y0: {:.1f}, x0: {:.1f}",
0202                      linePars[toUnderlying(ParIndex::theta)] / 1._degree,
0203                      linePars[toUnderlying(ParIndex::phi)] / 1._degree,
0204                      linePars[toUnderlying(ParIndex::y0)],
0205                      linePars[toUnderlying(ParIndex::x0)])
0206       << " --> " << toString(line.position()) << " + "
0207       << toString(line.direction()));
0208 
0209   return line;
0210 }
0211 
0212 class MeasurementGenerator {
0213  public:
0214   struct Config {
0215     /// @brief Create straw measurements
0216     bool createStraws{true};
0217     /// @brief Smear the straw radius
0218     bool smearRadius{true};
0219     /// @brief Straw measurement measures the coordinate along the wire
0220     bool twinStraw{false};
0221     /// @brief Resolution of the coordinate along the wire measurement
0222     double twinStrawReso{5._cm};
0223     /// @brief Create strip measurements
0224     bool createStrips{true};
0225     /// @brief Smear the strips around the pitch
0226     bool smearStrips{true};
0227     /// @brief Alternatively, discretize the strips onto a strip plane
0228     bool discretizeStrips{false};
0229     /// @brief Combine the two strip measurements to a single space point
0230     bool combineSpacePoints{false};
0231     /// @brief Create strip measurements constraining Loc0
0232     bool createStripsLoc0{true};
0233     /// @brief Create strip measurements constraining Loc1
0234     bool createStripsLoc1{true};
0235     /// @brief Pitch between two loc0 strips
0236     double stripPitchLoc0{4._cm};
0237     /// @brief Pitch between two loc1 strips
0238     double stripPitchLoc1{3._cm};
0239     /// @brief Direction of the strip if it measures loc1
0240     Vector3 stripDirLoc1{Vector3::UnitX()};
0241     /// @brief Direction of the strip if it measures loc0
0242     Vector3 stripDirLoc0{Vector3::UnitY()};
0243   };
0244   /// @brief Extrapolate the straight line track through the straw layers to
0245   ///        evaluate which tubes were crossed by the track. The straw layers
0246   ///        are staggered in the z-direction and each layer expands in y. To
0247   ///        estimate, which tubes were crossed, the track is extrapolated to
0248   ///        the z-plane below & above the straw wires. Optionally, the true
0249   ///        drift-radius can be smeared assuming a Gaussian with a drift-radius
0250   ///        dependent uncertainty.
0251   /// @param line: The track to extrapolate
0252   /// @param engine: Random number generator to smear the drift radius
0253   /// @param smearRadius: If true, the drift radius is smeared with a Gaussian
0254   /// @param createStrips: If true the strip measurements are created
0255   static Container_t spawn(const Line_t& line, const double /*t0*/,
0256                            RandomEngine& engine, const Config& genCfg) {
0257     /// Direction vector to go a positive step in the tube honeycomb grid
0258     const Vector3 posStaggering{0., std::cos(60._degree), std::sin(60._degree)};
0259     /// Direction vector to go a negative step in the tube honeycomb grid
0260     const Vector3 negStaggering{0., -std::cos(60._degree),
0261                                 std::sin(60._degree)};
0262     /// Number of tube layers per multilayer
0263     constexpr std::size_t nLayersPerMl = 8;
0264     /// Number of overall tubelayers
0265     constexpr std::size_t nTubeLayers = nLayersPerMl * 2;
0266     /// Position in z of the first tube layer
0267     constexpr double chamberDistance = 3._m;
0268     /// Radius of each straw
0269     constexpr double tubeRadius = 15._mm;
0270     /// Distance between the first <nLayersPerMl> layers and the second pack
0271     constexpr double tubeLayerDist = 1.2_m;
0272     /// Distance between the tube multilayers and to the first strip layer
0273     constexpr double tubeStripDist = 30._cm;
0274     /// Distance between two strip layers
0275     constexpr double stripLayDist = 0.5_cm;
0276     /// Number of strip layers on each side
0277     constexpr std::size_t nStripLay = 8;
0278 
0279     std::array<Vector3, nTubeLayers> tubePositions{
0280         filledArray<Vector3, nTubeLayers>(chamberDistance * Vector3::UnitZ())};
0281     /// Fill the positions of the reference tubes 1
0282     for (std::size_t l = 1; l < nTubeLayers; ++l) {
0283       const Vector3& layStag{l % 2 == 1 ? posStaggering : negStaggering};
0284       tubePositions[l] = tubePositions[l - 1] + 2. * tubeRadius * layStag;
0285 
0286       if (l == nLayersPerMl) {
0287         tubePositions[l] += tubeLayerDist * Vector3::UnitZ();
0288       }
0289     }
0290     /// Print the staggering
0291     ACTS_DEBUG("##############################################");
0292 
0293     for (std::size_t l = 0; l < nTubeLayers; ++l) {
0294       ACTS_DEBUG("  *** " << (l + 1) << " - " << toString(tubePositions[l]));
0295     }
0296     ACTS_DEBUG("##############################################");
0297 
0298     Container_t measurements{};
0299     /// Extrapolate the track to the z-planes of the tubes and determine which
0300     /// tubes were actually hit
0301     if (genCfg.createStraws) {
0302       for (const auto& stag : tubePositions) {
0303         auto planeExtpLow =
0304             intersectPlane(line.position(), line.direction(), Vector3::UnitZ(),
0305                            stag.z() - tubeRadius);
0306         auto planeExtpHigh =
0307             intersectPlane(line.position(), line.direction(), Vector3::UnitZ(),
0308                            stag.z() + tubeRadius);
0309         ACTS_DEBUG("spawn() - Extrapolated to plane "
0310                    << toString(planeExtpLow.position()) << " "
0311                    << toString(planeExtpHigh.position()));
0312 
0313         const auto dToFirstLow = static_cast<int>(std::ceil(
0314             (planeExtpLow.position().y() - stag.y()) / (2. * tubeRadius)));
0315         const auto dToFirstHigh = static_cast<int>(std::ceil(
0316             (planeExtpHigh.position().y() - stag.y()) / (2. * tubeRadius)));
0317         /// Does the track go from left to right or right to left?
0318         const int dT = dToFirstHigh > dToFirstLow ? 1 : -1;
0319         /// Loop over the candidate tubes and check each one whether the track
0320         /// actually crossed them. Then generate the circle and optionally smear
0321         /// the radius
0322         for (int tN = dToFirstLow - dT; tN != dToFirstHigh + 2 * dT; tN += dT) {
0323           Vector3 tube = stag + 2. * tN * tubeRadius * Vector3::UnitY();
0324           const double rad = Acts::abs(signedDistance(
0325               tube, Vector3::UnitX(), line.position(), line.direction()));
0326           if (rad > tubeRadius) {
0327             continue;
0328           }
0329 
0330           const double smearedR =
0331               genCfg.smearRadius
0332                   ? Acts::abs(
0333                         normal_t{rad, SpCalibrator::driftUncert(rad)}(engine))
0334                   : rad;
0335           if (smearedR > tubeRadius) {
0336             continue;
0337           }
0338           ///
0339           if (genCfg.twinStraw) {
0340             const auto iSectWire = lineIntersect<3>(
0341                 line.position(), line.direction(), tube, Vector3::UnitX());
0342             tube = iSectWire.position();
0343             tube[eX] = normal_t{tube[eX], genCfg.twinStrawReso}(engine);
0344           }
0345           ACTS_DEBUG("spawn() - Tube position: " << toString(tube)
0346                                                  << ", radius: " << rad);
0347 
0348           measurements.emplace_back(std::make_unique<FitTestSpacePoint>(
0349               tube, smearedR, SpCalibrator::driftUncert(smearedR),
0350               genCfg.twinStraw
0351                   ? std::make_optional<double>(genCfg.twinStrawReso)
0352                   : std::nullopt));
0353         }
0354       }
0355     }
0356     if (genCfg.createStrips) {
0357       ///@brief Helper function to discretize the extrapolated position on the strip plane to actual strips.
0358       ///       The function returns the local coordinate in the picked plane
0359       ///       projection
0360       /// @param extPos: Extrapolated position of the straight line in the plane
0361       /// @param loc1: Boolean to fetch either the loc1 projection or the loc0 projection
0362       auto discretize = [&genCfg, &engine](const Vector3& extPos,
0363                                            const bool loc1) -> Vector3 {
0364         const double pitch =
0365             loc1 ? genCfg.stripPitchLoc1 : genCfg.stripPitchLoc0;
0366         assert(pitch > 0.);
0367 
0368         const Vector3& stripDir =
0369             loc1 ? genCfg.stripDirLoc1 : genCfg.stripDirLoc0;
0370         const Vector3 stripNormal = stripDir.cross(Vector3::UnitZ());
0371         const double dist = stripNormal.dot(extPos);
0372         if (genCfg.smearStrips) {
0373           return normal_t{dist, pitch / std::sqrt(12)}(engine)*stripNormal;
0374         }
0375         if (genCfg.discretizeStrips) {
0376           return pitch *
0377                  static_cast<int>(std::ceil((dist - 0.5 * pitch) / pitch)) *
0378                  stripNormal;
0379         }
0380         return dist * stripNormal;
0381       };
0382       /// Calculate the strip measurement's covariance
0383       const double stripCovLoc0 =
0384           Acts::square(genCfg.stripPitchLoc0) / std::sqrt(12.);
0385       const double stripCovLoc1 =
0386           Acts::square(genCfg.stripPitchLoc1) / std::sqrt(12.);
0387 
0388       for (std::size_t sL = 0; sL < nStripLay; ++sL) {
0389         const double distInZ = tubeStripDist + sL * stripLayDist;
0390         const double planeLow = tubePositions.front().z() - distInZ;
0391         const double planeHigh = tubePositions.back().z() + distInZ;
0392 
0393         for (const double plane : {planeLow, planeHigh}) {
0394           const auto extp = intersectPlane(line.position(), line.direction(),
0395                                            Vector3::UnitZ(), plane);
0396           ACTS_VERBOSE("spawn() - Propagated line to "
0397                        << toString(extp.position()) << ".");
0398           if (genCfg.combineSpacePoints) {
0399             const Vector3 extpPos{discretize(extp.position(), false) +
0400                                   discretize(extp.position(), true) +
0401                                   plane * Vector3::UnitZ()};
0402             measurements.emplace_back(std::make_unique<FitTestSpacePoint>(
0403                 extpPos, genCfg.stripDirLoc0, genCfg.stripDirLoc1, stripCovLoc0,
0404                 stripCovLoc1));
0405           } else {
0406             if (genCfg.createStripsLoc0) {
0407               const Vector3 extpPos{discretize(extp.position(), false) +
0408                                     plane * Vector3::UnitZ()};
0409               measurements.emplace_back(std::make_unique<FitTestSpacePoint>(
0410                   extpPos, genCfg.stripDirLoc0,
0411                   genCfg.stripDirLoc0.cross(Vector3::UnitZ()), stripCovLoc0,
0412                   0.));
0413               const auto& nM{*measurements.back()};
0414               ACTS_VERBOSE("spawn() - Created loc0 strip @"
0415                            << toString(nM.localPosition())
0416                            << ", dir: " << toString(nM.sensorDirection())
0417                            << ", to-next:" << toString(nM.toNextSensor())
0418                            << " -> covariance: " << nM.covariance()[0] << ".");
0419             }
0420             if (genCfg.createStripsLoc1) {
0421               const Vector3 extpPos{discretize(extp.position(), true) +
0422                                     plane * Vector3::UnitZ()};
0423               measurements.emplace_back(std::make_unique<FitTestSpacePoint>(
0424                   extpPos, genCfg.stripDirLoc1,
0425                   genCfg.stripDirLoc1.cross(Vector3::UnitZ()), 0.,
0426                   stripCovLoc1));
0427               const auto& nM{*measurements.back()};
0428               ACTS_VERBOSE("spawn() - Created loc1 strip @"
0429                            << toString(nM.localPosition())
0430                            << ", dir: " << toString(nM.sensorDirection())
0431                            << ", to-next:" << toString(nM.toNextSensor())
0432                            << " -> covariance: " << nM.covariance()[1] << ".");
0433             }
0434           }
0435         }
0436       }
0437     }
0438     ACTS_DEBUG("Track hit in total " << measurements.size() << " sensors.");
0439     std::ranges::sort(measurements, [&line](const auto& spA, const auto& spB) {
0440       return line.direction().dot(spA->localPosition() - line.position()) <
0441              line.direction().dot(spB->localPosition() - line.position());
0442     });
0443     return measurements;
0444   }
0445 };
0446 
0447 using GenCfg_t = MeasurementGenerator::Config;
0448 
0449 /// @brief Construct the start parameters from the hit container && the true trajectory
0450 /// @param line: True trajectory to pick-up the correct left/right ambiguity for the straw seed hits
0451 /// @param hits: List of measurements to be used for fitting
0452 ParamVec_t startParameters(const Line_t& line, const Container_t& hits) {
0453   ParamVec_t pars{};
0454 
0455   double tanPhi{0.};
0456   double tanTheta{0.};
0457   /// Setup the seed parameters in x0 && phi
0458   auto firstPhi = std::ranges::find_if(
0459       hits, [](const auto& sp) { return sp->measuresLoc0(); });
0460   auto lastPhi =
0461       std::ranges::find_if(std::ranges::reverse_view(hits),
0462                            [](const auto& sp) { return sp->measuresLoc0(); });
0463 
0464   if (firstPhi != hits.end() && lastPhi != hits.rend()) {
0465     const Vector3 firstToLastPhi =
0466         (**lastPhi).localPosition() - (**firstPhi).localPosition();
0467     tanPhi = firstToLastPhi.x() / firstToLastPhi.z();
0468     /// -> x = tanPhi * z + x_{0} ->
0469     pars[toUnderlying(FitParIndex::x0)] =
0470         (**lastPhi).localPosition().x() -
0471         (**lastPhi).localPosition().z() * tanPhi;
0472   }
0473   /// Setup the seed parameters in y0 && theta
0474   auto firstTube =
0475       std::ranges::find_if(hits, [](const auto& sp) { return sp->isStraw(); });
0476   auto lastTube =
0477       std::ranges::find_if(std::ranges::reverse_view(hits),
0478                            [](const auto& sp) { return sp->isStraw(); });
0479 
0480   if (firstTube != hits.end() && lastTube != hits.rend()) {
0481     const int signFirst =
0482         CompSpacePointAuxiliaries::strawSign(line, **firstTube);
0483     const int signLast = CompSpacePointAuxiliaries::strawSign(line, **lastTube);
0484 
0485     auto seedPars = CompositeSpacePointLineSeeder::constructTangentLine(
0486         **lastTube, **firstTube,
0487         CompositeSpacePointLineSeeder::encodeAmbiguity(signLast, signFirst));
0488     tanTheta = std::tan(seedPars.theta);
0489     pars[toUnderlying(FitParIndex::y0)] = seedPars.y0;
0490   } else {
0491     auto firstEta = std::ranges::find_if(hits, [](const auto& sp) {
0492       return !sp->isStraw() && sp->measuresLoc1();
0493     });
0494     auto lastEta = std::ranges::find_if(
0495         std::ranges::reverse_view(hits),
0496         [](const auto& sp) { return !sp->isStraw() && sp->measuresLoc1(); });
0497 
0498     if (firstEta != hits.end() && lastEta != hits.rend()) {
0499       const Vector3 firstToLastEta =
0500           (**lastEta).localPosition() - (**firstEta).localPosition();
0501       tanTheta = firstToLastEta.y() / firstToLastEta.z();
0502       /// -> y = tanTheta * z + y_{0} ->
0503       pars[toUnderlying(FitParIndex::y0)] =
0504           (**lastEta).localPosition().y() -
0505           (**lastEta).localPosition().z() * tanTheta;
0506     }
0507   }
0508 
0509   const Vector3 seedDir = makeDirectionFromAxisTangents(tanPhi, tanTheta);
0510   pars[toUnderlying(FitParIndex::theta)] = theta(seedDir);
0511   pars[toUnderlying(FitParIndex::phi)] = phi(seedDir);
0512   return pars;
0513 }
0514 
0515 BOOST_AUTO_TEST_SUITE(SeedingSuite)
0516 
0517 BOOST_AUTO_TEST_CASE(SeedTangents) {
0518   RandomEngine engine{1602};
0519   constexpr double tolerance = 1.e-3;
0520 
0521   using Seeder = CompositeSpacePointLineSeeder;
0522   using SeedAux = CompSpacePointAuxiliaries;
0523   using enum Seeder::TangentAmbi;
0524   GenCfg_t genCfg{};
0525   genCfg.createStrips = false;
0526   genCfg.createStraws = true;
0527   genCfg.smearRadius = false;
0528 
0529   for (std::size_t evt = 0; evt < nEvents; ++evt) {
0530     const auto line = generateLine(engine);
0531     auto testTubes = MeasurementGenerator::spawn(line, 0._ns, engine, genCfg);
0532     const double lineTanTheta = line.direction().y() / line.direction().z();
0533     const double lineY0 = line.position().y();
0534     for (std::size_t m1 = testTubes.size() - 1; m1 > testTubes.size() / 2;
0535          --m1) {
0536       for (std::size_t m2 = 0; m2 < m1; ++m2) {
0537         const auto& bottomTube = *testTubes[m2];
0538         const auto& topTube = *testTubes[m1];
0539         const int signTop = SeedAux::strawSign(line, topTube);
0540         const int signBot = SeedAux::strawSign(line, bottomTube);
0541         const auto trueAmbi = Seeder::encodeAmbiguity(signTop, signBot);
0542 
0543         ACTS_DEBUG(__func__ << "() " << __LINE__ << " - "
0544                             << std::format("bottom tube @ {:}, r: {:.3f}({:})",
0545                                            toString(bottomTube.localPosition()),
0546                                            (signBot * bottomTube.driftRadius()),
0547                                            signBot > 0 ? "R" : "L")
0548                             << ", "
0549                             << std::format("top tube @ {:}, r: {:.3f} ({:})",
0550                                            toString(topTube.localPosition()),
0551                                            (signTop * topTube.driftRadius()),
0552                                            signTop > 0 ? "R" : "L"));
0553 
0554         bool seenTruePars{false};
0555         std::set<std::pair<double, double>> fourSeedPars{};
0556         for (const auto ambi : {LL, RL, LR, RR}) {
0557           const auto pars =
0558               Seeder::constructTangentLine(topTube, bottomTube, ambi);
0559 
0560           const bool isTruePars =
0561               Acts::abs(std::tan(pars.theta) - lineTanTheta) < tolerance &&
0562               Acts::abs(lineY0 - pars.y0) < tolerance;
0563           seenTruePars |= isTruePars;
0564           ACTS_VERBOSE(__func__
0565                        << "() " << __LINE__ << " - Test ambiguity "
0566                        << CompositeSpacePointLineSeeder::toString(ambi)
0567                        << " -> "
0568                        << std::format("theta: {:.3f}, ", pars.theta / 1._degree)
0569                        << std::format("tanTheta: {:.3f}, ",
0570                                       std::tan(pars.theta))
0571                        << std::format("y0: {:.3f}", pars.y0)
0572                        << (!isTruePars ? (ambi == trueAmbi ? " xxxxxx" : "")
0573                                        : " <-------"));
0574           const Vector3 seedPos = pars.y0 * Vector3::UnitY();
0575           const Vector3 seedDir =
0576               makeDirectionFromAxisTangents(0., std::tan(pars.theta));
0577 
0578           const double chi2Top = SeedAux::chi2Term(seedPos, seedDir, topTube);
0579           const double chi2Bot =
0580               SeedAux::chi2Term(seedPos, seedDir, bottomTube);
0581           ACTS_VERBOSE(__func__ << "() " << __LINE__
0582                                 << " - Resulting chi2 top: " << chi2Top
0583                                 << ", bottom: " << chi2Bot);
0584           BOOST_CHECK_LE(chi2Top, 1.e-17);
0585           BOOST_CHECK_LE(chi2Bot, 1.e-17);
0586           BOOST_CHECK_EQUAL(
0587               fourSeedPars.insert(std::make_pair(pars.theta, pars.y0)).second,
0588               true);
0589         }
0590         BOOST_CHECK_EQUAL(seenTruePars, true);
0591         BOOST_CHECK_EQUAL(fourSeedPars.size(), 4);
0592         const auto seedPars =
0593             Seeder::constructTangentLine(topTube, bottomTube, trueAmbi);
0594         /// Construct line parameters
0595         ACTS_DEBUG(__func__
0596                    << "() " << __LINE__ << " - Line tan theta: " << lineTanTheta
0597                    << ", reconstructed theta: " << std::tan(seedPars.theta)
0598                    << ", line y0: " << lineY0
0599                    << ", reconstructed y0: " << seedPars.y0);
0600         BOOST_CHECK_CLOSE(std::tan(seedPars.theta), lineTanTheta, tolerance);
0601         BOOST_CHECK_CLOSE(seedPars.y0, lineY0, tolerance);
0602       }
0603     }
0604   }
0605 }
0606 
0607 #define DECLARE_BRANCH(dType, bName) \
0608   dType bName{};                     \
0609   outTree->Branch(#bName, &bName);
0610 
0611 void runFitTest(const Fitter_t::Config& fitCfg, const GenCfg_t& genCfg,
0612                 const std::string& testName, RandomEngine& engine,
0613                 TFile& outFile) {
0614   Fitter_t fitter{fitCfg, getDefaultLogger(
0615                               std::format("LineFitter_{:}", testName), logLvl)};
0616 
0617   auto outTree = std::make_unique<TTree>(
0618       std::format("{:}Tree", testName).c_str(), "MonitorTree");
0619 
0620   TimePoint_t start = std::chrono::system_clock::now();
0621 
0622   ACTS_INFO("Start test " << testName << ".");
0623 
0624   DECLARE_BRANCH(double, trueY0);
0625   DECLARE_BRANCH(double, trueX0);
0626   DECLARE_BRANCH(double, trueTheta);
0627   DECLARE_BRANCH(double, truePhi);
0628   DECLARE_BRANCH(double, trueT0);
0629   DECLARE_BRANCH(double, trueProjTheta);
0630   DECLARE_BRANCH(double, trueProjPhi);
0631 
0632   DECLARE_BRANCH(double, recoY0);
0633   DECLARE_BRANCH(double, recoX0);
0634   DECLARE_BRANCH(double, recoTheta);
0635   DECLARE_BRANCH(double, recoPhi);
0636   DECLARE_BRANCH(double, recoT0);
0637   DECLARE_BRANCH(double, recoProjTheta);
0638   DECLARE_BRANCH(double, recoProjPhi);
0639 
0640   DECLARE_BRANCH(double, sigmaY0);
0641   DECLARE_BRANCH(double, sigmaX0);
0642   DECLARE_BRANCH(double, sigmaTheta);
0643   DECLARE_BRANCH(double, sigmaPhi);
0644   DECLARE_BRANCH(double, sigmaT0);
0645 
0646   DECLARE_BRANCH(double, chi2);
0647   DECLARE_BRANCH(unsigned, nIter);
0648   DECLARE_BRANCH(unsigned, nDoF);
0649 
0650   /// @brief Fill the parameter array to the tree variables
0651   /// @param pars: Parameter array to safe
0652   /// @param y0: Reference to the variable storing y0
0653   /// @param x0: Reference to the variable storing x0
0654   /// @param theta: Reference to the variable storing theta
0655   /// @param phi: Reference to the variable storing phi
0656   auto fillPars = [](const auto pars, double& y0, double& x0, double& theta,
0657                      double& phi) {
0658     y0 = pars[toUnderlying(FitParIndex::y0)];
0659     x0 = pars[toUnderlying(FitParIndex::x0)];
0660     theta = pars[toUnderlying(FitParIndex::theta)] / 1._degree;
0661     phi = pars[toUnderlying(FitParIndex::phi)] / 1._degree;
0662   };
0663   /// @brief Fill the
0664   auto fillProjected = [](const auto pars, double& projTheta, double& projPhi) {
0665     auto dir =
0666         makeDirectionFromPhiTheta(pars[toUnderlying(FitParIndex::phi)],
0667                                   pars[toUnderlying(FitParIndex::theta)]);
0668     projTheta = std::atan(dir[ePos1] / dir[ePos2]) / 1._degree;
0669     projPhi = std::atan(dir[ePos0] / dir[ePos2]) / 1._degree;
0670   };
0671   auto calibrator = std::make_unique<SpCalibrator>();
0672   for (std::size_t evt = 0; evt < nEvents; ++evt) {
0673     const auto line = generateLine(engine);
0674     fillPars(line.parameters(), trueY0, trueX0, trueTheta, truePhi);
0675     fillProjected(line.parameters(), trueProjTheta, trueProjPhi);
0676     const double t0 = uniform{-50._ns, 50._ns}(engine);
0677     trueT0 = t0 / 1._ns;
0678 
0679     using FitOpts_t = Fitter_t::FitOptions<Container_t, SpCalibrator>;
0680 
0681     FitOpts_t fitOpts{};
0682     fitOpts.calibrator = calibrator.get();
0683     fitOpts.measurements =
0684         MeasurementGenerator::spawn(line, t0, engine, genCfg);
0685     fitOpts.startParameters = startParameters(line, fitOpts.measurements);
0686     fillPars(fitOpts.startParameters, recoY0, recoX0, recoTheta, recoPhi);
0687 
0688     //
0689 
0690     auto result = fitter.fit(std::move(fitOpts));
0691     if (!result.converged) {
0692       continue;
0693     }
0694 
0695     fillPars(result.parameters, recoY0, recoX0, recoTheta, recoPhi);
0696     fillProjected(result.parameters, recoProjTheta, recoProjPhi);
0697 
0698     recoT0 = result.parameters[toUnderlying(FitParIndex::t0)] / 1._ns;
0699 
0700     auto extractUncert = [&result](const auto idx) {
0701       return std::sqrt(result.covariance(toUnderlying(idx), toUnderlying(idx)));
0702     };
0703     sigmaY0 = extractUncert(FitParIndex::y0);
0704     sigmaX0 = extractUncert(FitParIndex::x0);
0705     sigmaTheta = extractUncert(FitParIndex::theta) / 1._degree;
0706     sigmaPhi = extractUncert(FitParIndex::phi) / 1._degree;
0707     sigmaT0 = extractUncert(FitParIndex::t0) / 1._ns;
0708 
0709     chi2 = result.chi2;
0710     nDoF = result.nDoF;
0711     nIter = result.nIter;
0712 
0713     outTree->Fill();
0714     if ((evt + 1) % 10 == 0u) {
0715       ACTS_INFO("Processed " << (evt + 1) << "/" << nEvents << " events.");
0716     }
0717   }
0718 
0719   TimePoint_t end = std::chrono::system_clock::now();  // timing: get end time
0720   auto diff =
0721       std::chrono::duration_cast<std::chrono::seconds>(end - start).count();
0722 
0723   outFile.WriteObject(outTree.get(), outTree->GetName());
0724   ACTS_INFO("Test finished. " << outTree->GetEntries()
0725                               << " tracks written. Test took " << diff
0726                               << " seconds.");
0727 }
0728 #undef DECLARE_BRANCH
0729 
0730 BOOST_AUTO_TEST_CASE(SimpleLineFit) {
0731   auto outFile =
0732       std::make_unique<TFile>("StrawLineFitterTest.root", "RECREATE");
0733 
0734   Fitter_t::Config fitCfg{};
0735   fitCfg.useHessian = true;
0736   fitCfg.calcAlongStraw = true;
0737 
0738   GenCfg_t genCfg{};
0739   genCfg.twinStraw = false;
0740   genCfg.createStrips = false;
0741   // 2D straw only test
0742   {
0743     RandomEngine engine{1602};
0744     runFitTest(fitCfg, genCfg, "StrawOnlyTest", engine, *outFile);
0745   }
0746   // fast straw only test
0747   {
0748     fitCfg.useFastFitter = true;
0749     RandomEngine engine{1503};
0750     runFitTest(fitCfg, genCfg, "FastStrawOnlyTest", engine, *outFile);
0751   }
0752   // 2D straws + twin measurement test
0753 
0754   {
0755     fitCfg.useFastFitter = true;
0756     genCfg.twinStraw = true;
0757     RandomEngine engine{1701};
0758     runFitTest(fitCfg, genCfg, "StrawAndTwinTest", engine, *outFile);
0759   }
0760   genCfg.createStrips = true;
0761   genCfg.twinStraw = false;
0762   genCfg.combineSpacePoints = false;
0763   // 1D straws + single strip measurements
0764   {
0765     RandomEngine engine{1404};
0766     runFitTest(fitCfg, genCfg, "StrawAndStripTest", engine, *outFile);
0767   }
0768   // Strip only
0769   {
0770     genCfg.createStraws = false;
0771     genCfg.combineSpacePoints = true;
0772     genCfg.stripPitchLoc1 = 500._um;
0773     RandomEngine engine{2070};
0774     runFitTest(fitCfg, genCfg, "StripOnlyTest", engine, *outFile);
0775   }
0776   // Strip stereo test
0777   {
0778     genCfg.stripDirLoc1 = makeDirectionFromPhiTheta(60._degree, 90._degree);
0779     RandomEngine engine{2225};
0780     runFitTest(fitCfg, genCfg, "StereoStripTest", engine, *outFile);
0781   }
0782 }
0783 
0784 BOOST_AUTO_TEST_SUITE_END()
0785 }  // namespace ActsTests