Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-11-09 09:29:01

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/Algebra.hpp"
0012 #include "Acts/Definitions/TrackParametrization.hpp"
0013 #include "Acts/EventData/TrackContainer.hpp"
0014 #include "Acts/EventData/TrackStatePropMask.hpp"
0015 #include "Acts/EventData/VectorMultiTrajectory.hpp"
0016 #include "Acts/EventData/VectorTrackContainer.hpp"
0017 #include "Acts/Geometry/GeometryContext.hpp"
0018 #include "Acts/Surfaces/PerigeeSurface.hpp"
0019 #include "Acts/TrackFitting/GainMatrixSmoother.hpp"
0020 #include "Acts/TrackFitting/MbfSmoother.hpp"
0021 #include "Acts/TrackFitting/TrackletJoiner.hpp"
0022 #include "Acts/TrackFitting/detail/TrackletJoining.hpp"
0023 #include "ActsTests/CommonHelpers/FloatComparisons.hpp"
0024 
0025 #include <algorithm>
0026 #include <array>
0027 #include <cstddef>
0028 #include <limits>
0029 #include <map>
0030 #include <vector>
0031 
0032 namespace {
0033 
0034 using namespace Acts;
0035 
0036 const GeometryContext gctx = GeometryContext::dangerouslyDefaultConstruct();
0037 
0038 constexpr std::size_t kDim = eBoundSize;
0039 const std::array<BoundIndices, 2> kProjector{eBoundLoc0, eBoundLoc1};
0040 
0041 /// One node of a linear-Gaussian model: transport from its parent, process
0042 /// noise, and an optional two-dimensional measurement of (loc0, loc1).
0043 struct Node {
0044   int parent = -1;  // -1: the node carries the prior
0045   BoundMatrix F = BoundMatrix::Identity();
0046   BoundMatrix Q = BoundMatrix::Zero();
0047   bool hasMeasurement = false;
0048   Vector2 y = Vector2::Zero();
0049   SquareMatrix2 V = SquareMatrix2::Identity();
0050 };
0051 
0052 BoundMatrix makeJacobian(double scale) {
0053   BoundMatrix f = BoundMatrix::Identity();
0054   f(eBoundLoc0, eBoundPhi) = 0.3 * scale;
0055   f(eBoundLoc1, eBoundTheta) = 0.2 * scale;
0056   f(eBoundPhi, eBoundQOverP) = 0.1 * scale;
0057   f(eBoundLoc0, eBoundQOverP) = 0.05 * scale;
0058   f(eBoundTime, eBoundLoc1) = 0.1 * scale;
0059   return f;
0060 }
0061 
0062 BoundMatrix makeNoise(double scale) {
0063   BoundVector d;
0064   d << 1e-3, 1e-3, 1e-4, 1e-4, 1e-5, 1e-3;
0065   return (d * scale).asDiagonal();
0066 }
0067 
0068 const BoundVector kPriorMean = [] {
0069   BoundVector m;
0070   m << 0.1, -0.2, 1.3, 0.7, 0.05, 0.0;
0071   return m;
0072 }();
0073 
0074 const BoundMatrix kPriorCov = [] {
0075   BoundVector d;
0076   d << 0.04, 0.09, 0.01, 0.01, 1e-4, 0.02;
0077   return BoundMatrix(d.asDiagonal());
0078 }();
0079 
0080 Matrix<2, kDim> projector() {
0081   Matrix<2, kDim> h = Matrix<2, kDim>::Zero();
0082   h(0, eBoundLoc0) = 1;
0083   h(1, eBoundLoc1) = 1;
0084   return h;
0085 }
0086 
0087 /// Variants of the model
0088 enum class Variant {
0089   /// nodes 0 to 3 are the first tracklet in filter order, node 0 carries the
0090   /// prior and is the shared state; nodes 4 to 6 are the second tracklet,
0091   /// filtered from node 0. Nodes 1 and 5 are holes.
0092   Base,
0093   /// Base plus node 7, a hole after the last measurement of the first
0094   /// tracklet. It has no smoothed component, as after `smoothTrack`.
0095   TrailingHole,
0096   /// Base plus node 7, a hole before the first measurement. It carries the
0097   /// prior, node 0 is transported from it, and node 0 stays the shared state.
0098   StemHole,
0099   /// Only node 0 belongs to the first tracklet; nodes 1 to 3 are unobserved.
0100   SingleMeasurement,
0101 };
0102 
0103 /// The model
0104 std::vector<Node> makeModel(Variant variant) {
0105   std::vector<Node> nodes(7);
0106   const SquareMatrix2 v = Vector2(4e-3, 9e-3).asDiagonal();
0107 
0108   nodes[0] = {-1,   BoundMatrix::Identity(), BoundMatrix::Zero(),
0109               true, Vector2(0.12, -0.23),    v};
0110   nodes[1] = {0, makeJacobian(1.0), makeNoise(1.0), false, Vector2::Zero(), v};
0111   nodes[2] = {1,    makeJacobian(0.8),   makeNoise(1.5),
0112               true, Vector2(0.55, -0.1), v};
0113   nodes[3] = {2,    makeJacobian(1.2),  makeNoise(0.7),
0114               true, Vector2(0.9, 0.05), v};
0115   nodes[4] = {0,    makeJacobian(-1.1),    makeNoise(1.2),
0116               true, Vector2(-0.25, -0.35), v};
0117   nodes[5] = {4, makeJacobian(-0.9), makeNoise(0.9), false, Vector2::Zero(), v};
0118   nodes[6] = {5,    makeJacobian(-1.3),  makeNoise(1.1),
0119               true, Vector2(-0.7, -0.5), v};
0120 
0121   if (variant == Variant::TrailingHole) {
0122     nodes.push_back(
0123         {3, makeJacobian(0.9), makeNoise(1.3), false, Vector2::Zero(), v});
0124   } else if (variant == Variant::StemHole) {
0125     // the stem hole carries the prior, node 0 is transported from it
0126     nodes.push_back({-1, BoundMatrix::Identity(), BoundMatrix::Zero(), false,
0127                      Vector2::Zero(), v});
0128     nodes[0].parent = 7;
0129     nodes[0].F = makeJacobian(0.7);
0130     nodes[0].Q = makeNoise(0.8);
0131   } else if (variant == Variant::SingleMeasurement) {
0132     nodes[2].hasMeasurement = false;
0133     nodes[3].hasMeasurement = false;
0134   }
0135   return nodes;
0136 }
0137 
0138 /// Dense joint posterior of all nodes given all measurements and the prior.
0139 std::pair<DynamicVector, DynamicMatrix> densePosterior(
0140     const std::vector<Node>& nodes) {
0141   const std::size_t n = nodes.size();
0142   const std::size_t dim = n * kDim;
0143 
0144   DynamicMatrix a = DynamicMatrix::Zero(dim, dim);
0145   DynamicMatrix w = DynamicMatrix::Zero(dim, dim);
0146   DynamicVector c = DynamicVector::Zero(dim);
0147   for (std::size_t i = 0; i < n; ++i) {
0148     const Node& node = nodes[i];
0149     if (node.parent < 0) {
0150       w.block<kDim, kDim>(i * kDim, i * kDim) = kPriorCov;
0151       c.segment<kDim>(i * kDim) = kPriorMean;
0152     } else {
0153       a.block<kDim, kDim>(i * kDim, node.parent * kDim) = node.F;
0154       w.block<kDim, kDim>(i * kDim, i * kDim) = node.Q;
0155     }
0156   }
0157 
0158   const DynamicMatrix g = (DynamicMatrix::Identity(dim, dim) - a).inverse();
0159   const DynamicVector priorMean = g * c;
0160   const DynamicMatrix priorCov = g * w * g.transpose();
0161 
0162   DynamicMatrix information = priorCov.inverse();
0163   DynamicVector informationVector = information * priorMean;
0164   const Matrix<2, kDim> h = projector();
0165   for (std::size_t i = 0; i < n; ++i) {
0166     const Node& node = nodes[i];
0167     if (!node.hasMeasurement) {
0168       continue;
0169     }
0170     const SquareMatrix2 vInv = node.V.inverse();
0171     information.block<kDim, kDim>(i * kDim, i * kDim) +=
0172         h.transpose() * vInv * h;
0173     informationVector.segment<kDim>(i * kDim) += h.transpose() * vInv * node.y;
0174   }
0175 
0176   const DynamicMatrix cov = information.inverse();
0177   const DynamicVector mean = cov * informationVector;
0178   return {mean, cov};
0179 }
0180 
0181 using Container = TrackContainer<VectorTrackContainer, VectorMultiTrajectory,
0182                                  detail::ValueHolder>;
0183 
0184 /// Append one filtered state to a track, the way the CKF stores it.
0185 std::size_t appendState(Container::TrackProxy& track, const Node& node,
0186                         const BoundVector& prevMean, const BoundMatrix& prevCov,
0187                         TrackStatePropMask mask = TrackStatePropMask::All) {
0188   auto ts = track.appendTrackState(mask);
0189   ts.jacobian() = node.F;
0190 
0191   const BoundVector predicted = node.parent < 0 ? prevMean : node.F * prevMean;
0192   const BoundMatrix predictedCov =
0193       node.parent < 0 ? prevCov
0194                       : node.F * prevCov * node.F.transpose() + node.Q;
0195   ts.predicted() = predicted;
0196   ts.predictedCovariance() = predictedCov;
0197 
0198   if (node.hasMeasurement) {
0199     ts.typeFlags().setIsMeasurement();
0200     ts.allocateCalibrated(2);
0201     ts.calibrated<2>() = node.y;
0202     ts.calibratedCovariance<2>() = node.V;
0203     ts.setProjectorSubspaceIndices(kProjector);
0204 
0205     const Matrix<2, kDim> h = projector();
0206     const SquareMatrix2 s = h * predictedCov * h.transpose() + node.V;
0207     const Matrix<kDim, 2> k = predictedCov * h.transpose() * s.inverse();
0208     ts.filtered() = predicted + k * (node.y - h * predicted);
0209     ts.filteredCovariance() = (BoundMatrix::Identity() - k * h) * predictedCov;
0210   } else {
0211     ts.typeFlags().setIsHole();
0212     ts.filtered() = predicted;
0213     ts.filteredCovariance() = predictedCov;
0214   }
0215   return ts.index();
0216 }
0217 
0218 struct Tracklets {
0219   Container::TrackProxy first;
0220   Container::TrackStateProxy shared;
0221   Container::TrackProxy second;
0222   /// node of each track state index
0223   std::map<std::size_t, std::size_t> nodeOfState;
0224   /// states that must stay without smoothed parameters
0225   std::vector<std::size_t> unsmoothed;
0226 };
0227 
0228 /// Build the first tracklet, smooth it from its last measurement as
0229 /// `smoothTrack` does, and build the second tracklet from the smoothed
0230 /// shared state, which is the first measurement.
0231 template <typename first_smoother_t = MbfSmoother>
0232 Tracklets buildTracklets(Container& tc, const std::vector<Node>& nodes,
0233                          Variant variant) {
0234   std::map<std::size_t, std::size_t> nodeOfState;
0235   std::vector<std::size_t> unsmoothed;
0236 
0237   auto first = tc.makeTrack();
0238   BoundVector mean = kPriorMean;
0239   BoundMatrix cov = kPriorCov;
0240   std::vector<std::size_t> firstNodes = {0, 1, 2, 3};
0241   if (variant == Variant::TrailingHole) {
0242     firstNodes.push_back(7);
0243   } else if (variant == Variant::StemHole) {
0244     firstNodes.insert(firstNodes.begin(), 7);
0245   } else if (variant == Variant::SingleMeasurement) {
0246     firstNodes = {0};
0247   }
0248   std::size_t lastMeasurement = kTrackIndexInvalid;
0249   std::size_t sharedIndex = kTrackIndexInvalid;
0250   for (std::size_t node : firstNodes) {
0251     // a trailing hole is created without a smoothed component, as the CKF
0252     // does for every state
0253     const bool trailing = (variant == Variant::TrailingHole && node == 7);
0254     const std::size_t index = appendState(
0255         first, nodes[node], mean, cov,
0256         trailing ? (TrackStatePropMask::Predicted |
0257                     TrackStatePropMask::Filtered | TrackStatePropMask::Jacobian)
0258                  : TrackStatePropMask::All);
0259     nodeOfState[index] = node;
0260     auto ts = first.outermostTrackState();
0261     mean = ts.filtered();
0262     cov = ts.filteredCovariance();
0263     if (nodes[node].hasMeasurement) {
0264       lastMeasurement = index;
0265       if (sharedIndex == kTrackIndexInvalid) {
0266         sharedIndex = index;
0267       }
0268     }
0269     if (trailing) {
0270       unsmoothed.push_back(index);
0271     }
0272   }
0273   first.linkForward();
0274   BOOST_REQUIRE(
0275       first_smoother_t{}(gctx, tc.trackStateContainer(), lastMeasurement).ok());
0276 
0277   auto shared = tc.trackStateContainer().getTrackState(sharedIndex);
0278   BOOST_REQUIRE(shared.hasSmoothed());
0279   BOOST_REQUIRE(shared.typeFlags().isMeasurement());
0280 
0281   auto second = tc.makeTrack();
0282   mean = shared.smoothed();
0283   cov = shared.smoothedCovariance();
0284   for (std::size_t node = 4; node < 7; ++node) {
0285     const std::size_t index = appendState(second, nodes[node], mean, cov);
0286     nodeOfState[index] = node;
0287     auto ts = second.outermostTrackState();
0288     mean = ts.filtered();
0289     cov = ts.filteredCovariance();
0290   }
0291 
0292   return {first, shared, second, nodeOfState, unsmoothed};
0293 }
0294 
0295 /// Check one smoothed state against the dense joint posterior
0296 template <typename track_state_proxy_t>
0297 void checkStateAgainstDense(const track_state_proxy_t& ts, std::size_t node,
0298                             const DynamicVector& mean, const DynamicMatrix& cov,
0299                             double tol) {
0300   BOOST_REQUIRE(ts.hasSmoothed());
0301   const BoundVector expMean = mean.segment<kDim>(node * kDim);
0302   const BoundMatrix expCov = cov.block<kDim, kDim>(node * kDim, node * kDim);
0303   CHECK_CLOSE_ABS(ts.smoothed(), expMean, tol);
0304   CHECK_CLOSE_ABS(ts.smoothedCovariance(), expCov, tol);
0305   // the covariance is symmetric
0306   const BoundMatrix asym =
0307       ts.smoothedCovariance() - ts.smoothedCovariance().transpose();
0308   BOOST_CHECK_LT(asym.cwiseAbs().maxCoeff(), 1e-15);
0309 }
0310 
0311 /// Check every state of a joined track against the dense joint posterior.
0312 /// States listed as unsmoothed must have no smoothed component.
0313 void checkJoinedAgainstDense(const Container::TrackProxy& joined,
0314                              const std::map<std::size_t, std::size_t>& nodeOf,
0315                              const std::vector<std::size_t>& unsmoothed,
0316                              const std::vector<Node>& nodes, double tol,
0317                              std::size_t expectedStates) {
0318   const auto [mean, cov] = densePosterior(nodes);
0319   std::size_t count = 0;
0320   for (const auto& ts : joined.trackStatesReversed()) {
0321     ++count;
0322     if (std::find(unsmoothed.begin(), unsmoothed.end(), ts.index()) !=
0323         unsmoothed.end()) {
0324       BOOST_CHECK(!ts.hasSmoothed());
0325       continue;
0326     }
0327     BOOST_REQUIRE(nodeOf.contains(ts.index()));
0328     checkStateAgainstDense(ts, nodeOf.at(ts.index()), mean, cov, tol);
0329   }
0330   BOOST_CHECK_EQUAL(count, expectedStates);
0331 }
0332 
0333 /// Join in place and check against the dense posterior
0334 template <typename smoother_t>
0335 Tracklets joinAndCheck(Container& tc, Variant variant) {
0336   const auto nodes = makeModel(variant);
0337   auto tracklets = buildTracklets(tc, nodes, variant);
0338   TrackletJoiner joiner;
0339   BOOST_REQUIRE(joiner
0340                     .joinInPlace(gctx, tracklets.first, tracklets.shared,
0341                                  tracklets.second, smoother_t{})
0342                     .ok());
0343   // states before the shared state leave the chain in the link
0344   const std::size_t onChain = variant == Variant::StemHole ? 7 : nodes.size();
0345   checkJoinedAgainstDense(tracklets.first, tracklets.nodeOfState,
0346                           tracklets.unsmoothed, nodes, 1e-9, onChain);
0347   return tracklets;
0348 }
0349 
0350 }  // namespace
0351 
0352 namespace ActsTests {
0353 
0354 BOOST_AUTO_TEST_SUITE(TrackFittingSuite)
0355 
0356 using Smoothers = std::tuple<MbfSmoother, GainMatrixSmoother>;
0357 
0358 BOOST_AUTO_TEST_CASE_TEMPLATE(PriorNodeStepEqualsSmoothingWithPriorAsState,
0359                               smoother_t, Smoothers) {
0360   // a segment smoothed on its own, plus one step onto its prior node, equals
0361   // the smoothing of a chain that holds the prior node as a state
0362   const auto nodes = makeModel(Variant::Base);
0363   Container tc{VectorTrackContainer{}, VectorMultiTrajectory{}};
0364   auto full = tc.makeTrack();
0365   BoundVector mean = kPriorMean;
0366   BoundMatrix cov = kPriorCov;
0367   std::vector<std::size_t> fullIndices;
0368   for (std::size_t i = 0; i < 4; ++i) {
0369     fullIndices.push_back(appendState(full, nodes[i], mean, cov));
0370     mean = full.outermostTrackState().filtered();
0371     cov = full.outermostTrackState().filteredCovariance();
0372   }
0373   // the segment of nodes 2 and 3, predicted from the filtered node 1
0374   auto prior = tc.trackStateContainer().getTrackState(fullIndices[1]);
0375   const BoundVector priorMean = prior.filtered();
0376   const BoundMatrix priorCov = prior.filteredCovariance();
0377   auto segment = tc.makeTrack();
0378   mean = priorMean;
0379   cov = priorCov;
0380   std::size_t segmentFirst = kTrackIndexInvalid;
0381   for (std::size_t i = 2; i < 4; ++i) {
0382     const std::size_t index = appendState(segment, nodes[i], mean, cov);
0383     if (i == 2) {
0384       segmentFirst = index;
0385     }
0386     mean = segment.outermostTrackState().filtered();
0387     cov = segment.outermostTrackState().filteredCovariance();
0388   }
0389 
0390   BOOST_REQUIRE(
0391       MbfSmoother()(gctx, tc.trackStateContainer(), full.tipIndex()).ok());
0392   BOOST_REQUIRE(
0393       smoother_t{}(gctx, tc.trackStateContainer(), segment.tipIndex()).ok());
0394 
0395   auto segmentFirstState = tc.trackStateContainer().getTrackState(segmentFirst);
0396   auto step =
0397       detail::smoothPriorNode(AnyConstTrackStateProxy{segmentFirstState},
0398                               priorMean, priorCov, getDummyLogger());
0399   BOOST_REQUIRE(step.ok());
0400   CHECK_CLOSE_ABS(step->first, prior.smoothed(), 1e-10);
0401   CHECK_CLOSE_ABS(step->second, prior.smoothedCovariance(), 1e-10);
0402 
0403   // the segment states are smoothed as in the full chain
0404   std::vector<std::size_t> segmentIndices;
0405   for (const auto& ts : segment.trackStatesReversed()) {
0406     segmentIndices.push_back(ts.index());
0407   }
0408   for (std::size_t i = 0; i < 2; ++i) {
0409     auto a = tc.trackStateContainer().getTrackState(segmentIndices[i]);
0410     auto b = tc.trackStateContainer().getTrackState(fullIndices[3 - i]);
0411     CHECK_CLOSE_ABS(a.smoothed(), b.smoothed(), 1e-10);
0412     CHECK_CLOSE_ABS(a.smoothedCovariance(), b.smoothedCovariance(), 1e-10);
0413   }
0414 }
0415 
0416 BOOST_AUTO_TEST_CASE_TEMPLATE(JoinInPlaceMatchesJointPosterior, smoother_t,
0417                               Smoothers) {
0418   Container tc{VectorTrackContainer{}, VectorMultiTrajectory{}};
0419   auto tracklets = joinAndCheck<smoother_t>(tc, Variant::Base);
0420 
0421   // the link: the chain runs through the shared state into the second
0422   // tracklet, the stitch jacobian is the inverse transport, the stem is the
0423   // innermost state of the second tracklet, and the forward links follow
0424   const BoundMatrix secondStart = makeModel(Variant::Base)[4].F;
0425   BOOST_CHECK_EQUAL(tracklets.shared.previous(),
0426                     tracklets.second.outermostTrackState().index());
0427   CHECK_CLOSE_ABS(tracklets.shared.jacobian(), secondStart.inverse(), 1e-12);
0428   BOOST_CHECK_EQUAL(tracklets.first.stemIndex(), tracklets.second.stemIndex());
0429   std::vector<std::size_t> backward;
0430   for (const auto& ts : tracklets.first.trackStatesReversed()) {
0431     backward.push_back(ts.index());
0432   }
0433   std::vector<std::size_t> forward;
0434   for (const auto& ts : tracklets.first.trackStates()) {
0435     forward.push_back(ts.index());
0436   }
0437   BOOST_REQUIRE_EQUAL(backward.size(), 7u);
0438   BOOST_REQUIRE_EQUAL(forward.size(), 7u);
0439   for (std::size_t i = 0; i < 7; ++i) {
0440     BOOST_CHECK_EQUAL(forward[i], backward[6 - i]);
0441   }
0442 }
0443 
0444 BOOST_AUTO_TEST_CASE_TEMPLATE(JoinSkipsTrailingHoles, smoother_t, Smoothers) {
0445   // a hole after the last measurement has no smoothed component and stays
0446   // as it is; every other state matches the joint posterior
0447   Container tc{VectorTrackContainer{}, VectorMultiTrajectory{}};
0448   auto tracklets = joinAndCheck<smoother_t>(tc, Variant::TrailingHole);
0449   BOOST_REQUIRE_EQUAL(tracklets.unsmoothed.size(), 1u);
0450   BOOST_CHECK_EQUAL(tracklets.unsmoothed.front(), tracklets.first.tipIndex());
0451 }
0452 
0453 BOOST_AUTO_TEST_CASE_TEMPLATE(JoinSmoothsStemStatesBeforeShared, smoother_t,
0454                               Smoothers) {
0455   // a hole before the first measurement gets a smoothing step from the
0456   // corrected shared state and matches the joint posterior; the link then
0457   // takes it off the chain, because the second tracklet covers that side
0458   Container tc{VectorTrackContainer{}, VectorMultiTrajectory{}};
0459   auto tracklets = joinAndCheck<smoother_t>(tc, Variant::StemHole);
0460   const auto nodes = makeModel(Variant::StemHole);
0461   const auto [mean, cov] = densePosterior(nodes);
0462   std::size_t stemIndex = kTrackIndexInvalid;
0463   for (const auto& [index, node] : tracklets.nodeOfState) {
0464     if (node == 7) {
0465       stemIndex = index;
0466     }
0467   }
0468   BOOST_REQUIRE_NE(stemIndex, kTrackIndexInvalid);
0469   checkStateAgainstDense(tc.trackStateContainer().getTrackState(stemIndex), 7,
0470                          mean, cov, 1e-9);
0471   // off the chain: the shared state now links into the second tracklet
0472   BOOST_CHECK_NE(tracklets.shared.previous(), stemIndex);
0473 }
0474 
0475 BOOST_AUTO_TEST_CASE(JoinCopyLeavesInputsUntouched) {
0476   const auto nodes = makeModel(Variant::Base);
0477   Container tc{VectorTrackContainer{}, VectorMultiTrajectory{}};
0478   auto tracklets = buildTracklets(tc, nodes, Variant::Base);
0479   const BoundVector sharedBefore = tracklets.shared.smoothed();
0480   const std::size_t firstStates = tracklets.first.nTrackStates();
0481   const std::size_t secondStates = tracklets.second.nTrackStates();
0482   const std::size_t tracksBefore = tc.size();
0483 
0484   TrackletJoiner joiner;
0485   auto joined = joiner.joinCopy(gctx, tc, tracklets.first, tracklets.shared,
0486                                 tracklets.second);
0487   BOOST_REQUIRE(joined.ok());
0488 
0489   // the copy has new state indices: the joined chain reads the first
0490   // tracklet from its tip, then the second tracklet from its start
0491   std::vector<std::size_t> order;
0492   for (const auto& ts : tracklets.first.trackStatesReversed()) {
0493     order.push_back(tracklets.nodeOfState.at(ts.index()));
0494   }
0495   std::vector<std::size_t> secondOrder;
0496   for (const auto& ts : tracklets.second.trackStatesReversed()) {
0497     secondOrder.push_back(tracklets.nodeOfState.at(ts.index()));
0498   }
0499   order.insert(order.end(), secondOrder.rbegin(), secondOrder.rend());
0500   std::map<std::size_t, std::size_t> nodeOfCopy;
0501   std::size_t pos = 0;
0502   for (const auto& ts : joined->trackStatesReversed()) {
0503     nodeOfCopy[ts.index()] = order.at(pos++);
0504   }
0505   checkJoinedAgainstDense(*joined, nodeOfCopy, {}, nodes, 1e-9, nodes.size());
0506 
0507   CHECK_CLOSE_ABS(tracklets.shared.smoothed(), sharedBefore, 0.);
0508   BOOST_CHECK_EQUAL(tracklets.first.nTrackStates(), firstStates);
0509   BOOST_CHECK_EQUAL(tracklets.second.nTrackStates(), secondStates);
0510   // one joined track added, the shell of the second copy removed
0511   BOOST_CHECK_EQUAL(tc.size(), tracksBefore + 1);
0512   BOOST_CHECK_EQUAL(joined->nTrackStates(), firstStates + secondStates);
0513 }
0514 
0515 BOOST_AUTO_TEST_CASE(ConditioningRejectsStateOffTheChain) {
0516   const auto nodes = makeModel(Variant::Base);
0517   Container tc{VectorTrackContainer{}, VectorMultiTrajectory{}};
0518   auto tracklets = buildTracklets(tc, nodes, Variant::Base);
0519   auto res = detail::conditionSmoothedStatesOnSharedState(
0520       tc.trackStateContainer(), tracklets.second.tipIndex(),
0521       tracklets.first.tipIndex(), tracklets.shared.smoothed(),
0522       tracklets.shared.smoothedCovariance(), getDummyLogger());
0523   BOOST_CHECK(!res.ok());
0524 }
0525 
0526 BOOST_AUTO_TEST_CASE_TEMPLATE(ConditioningPreservesFilterHistory,
0527                               first_smoother_t, Smoothers) {
0528   for (const auto variant : {Variant::Base, Variant::SingleMeasurement}) {
0529     const auto nodes = makeModel(variant);
0530     Container tc{VectorTrackContainer{}, VectorMultiTrajectory{}};
0531     auto t = buildTracklets<first_smoother_t>(tc, nodes, variant);
0532     std::map<std::size_t, std::pair<BoundVector, BoundMatrix>> predicted;
0533     std::map<std::size_t, std::pair<BoundVector, BoundMatrix>> filtered;
0534     for (const auto& ts : t.first.trackStatesReversed()) {
0535       predicted[ts.index()] = {ts.predicted(), ts.predictedCovariance()};
0536       filtered[ts.index()] = {ts.filtered(), ts.filteredCovariance()};
0537     }
0538     BOOST_REQUIRE(
0539         TrackletJoiner{}.joinInPlace(gctx, t.first, t.shared, t.second).ok());
0540     checkJoinedAgainstDense(t.first, t.nodeOfState, {}, nodes, 1e-9,
0541                             variant == Variant::Base ? 7 : 4);
0542     for (const auto& [index, estimate] : filtered) {
0543       auto ts = tc.trackStateContainer().getTrackState(index);
0544       CHECK_CLOSE_ABS(ts.filtered(), estimate.first, 0.);
0545       CHECK_CLOSE_ABS(ts.filteredCovariance(), estimate.second, 0.);
0546       CHECK_CLOSE_ABS(ts.predicted(), predicted.at(index).first, 0.);
0547       CHECK_CLOSE_ABS(ts.predictedCovariance(), predicted.at(index).second, 0.);
0548     }
0549   }
0550 }
0551 
0552 BOOST_AUTO_TEST_CASE(JoinBuildsMissingForwardLinks) {
0553   Container tc{VectorTrackContainer{}, VectorMultiTrajectory{}};
0554   auto t = buildTracklets(tc, makeModel(Variant::Base), Variant::Base);
0555   // appendTrackState and the smoothers do not create forward links.
0556   for (auto ts : t.first.trackStatesReversed()) {
0557     ts.component<TrackIndexType>(detail_tp::kNextKey) = kTrackIndexInvalid;
0558   }
0559   t.first.stemIndex() = kTrackIndexInvalid;
0560   BOOST_REQUIRE(
0561       TrackletJoiner{}.joinInPlace(gctx, t.first, t.shared, t.second).ok());
0562   std::vector<std::size_t> backward;
0563   for (const auto& ts : t.first.trackStatesReversed()) {
0564     backward.push_back(ts.index());
0565   }
0566   std::vector<std::size_t> forward;
0567   for (const auto& ts : t.first.trackStates()) {
0568     forward.push_back(ts.index());
0569   }
0570   std::ranges::reverse(backward);
0571   BOOST_CHECK_EQUAL_COLLECTIONS(forward.begin(), forward.end(),
0572                                 backward.begin(), backward.end());
0573 }
0574 
0575 BOOST_AUTO_TEST_CASE(JoinEmptySecondPassKeepsStatesAndTargetEstimate) {
0576   Container tc{VectorTrackContainer{}, VectorMultiTrajectory{}};
0577   auto t = buildTracklets(tc, makeModel(Variant::Base), Variant::Base);
0578   auto second = tc.makeTrack();
0579   second.copyFromWithoutStates(t.first);
0580   const auto target = Surface::makeShared<PerigeeSurface>(Vector3::Zero());
0581   second.setReferenceSurface(target);
0582   second.parameters() = kPriorMean;
0583   second.covariance() = kPriorCov;
0584   const auto tip = t.first.tipIndex();
0585   const auto stem = t.first.stemIndex();
0586   std::vector<std::pair<BoundVector, BoundMatrix>> before;
0587   for (const auto& ts : t.first.trackStatesReversed()) {
0588     before.emplace_back(ts.smoothed(), ts.smoothedCovariance());
0589   }
0590   TrackletJoiner joiner;
0591   auto copy = joiner.joinCopy(gctx, tc, t.first, t.shared, second);
0592   BOOST_REQUIRE(copy.ok());
0593   BOOST_REQUIRE(joiner.joinInPlace(gctx, t.first, t.shared, second).ok());
0594   BOOST_CHECK_EQUAL(t.first.tipIndex(), tip);
0595   BOOST_CHECK_EQUAL(t.first.stemIndex(), stem);
0596   for (const auto& joined : {t.first, *copy}) {
0597     BOOST_CHECK_EQUAL(&joined.referenceSurface(), target.get());
0598     CHECK_CLOSE_ABS(joined.parameters(), kPriorMean, 0.);
0599     CHECK_CLOSE_ABS(joined.covariance(), kPriorCov, 0.);
0600     BOOST_REQUIRE_EQUAL(joined.nTrackStates(), before.size());
0601     std::size_t i = 0;
0602     for (const auto& ts : joined.trackStatesReversed()) {
0603       CHECK_CLOSE_ABS(ts.smoothed(), before[i].first, 0.);
0604       CHECK_CLOSE_ABS(ts.smoothedCovariance(), before[i].second, 0.);
0605       ++i;
0606     }
0607   }
0608 }
0609 
0610 BOOST_AUTO_TEST_CASE(RejectInvalidCovariances) {
0611   Container tc{VectorTrackContainer{}, VectorMultiTrajectory{}};
0612   auto t = buildTracklets(tc, makeModel(Variant::Base), Variant::Base);
0613   for (double variance : {0., -1., std::numeric_limits<double>::quiet_NaN(),
0614                           std::numeric_limits<double>::infinity()}) {
0615     BoundMatrix cov = BoundMatrix::Identity();
0616     cov(eBoundLoc0, eBoundLoc0) = variance;
0617     t.shared.smoothedCovariance() = cov;
0618     t.shared.predictedCovariance() = cov;
0619     BOOST_CHECK(!detail::makeSharedStateCondition(
0620                      AnyConstTrackStateProxy{t.shared}, kPriorMean, kPriorCov,
0621                      getDummyLogger())
0622                      .ok());
0623     BOOST_CHECK(!detail::smoothPriorNode(AnyConstTrackStateProxy{t.shared},
0624                                          kPriorMean, kPriorCov,
0625                                          getDummyLogger())
0626                      .ok());
0627   }
0628 }
0629 
0630 BOOST_AUTO_TEST_SUITE_END()
0631 
0632 }  // namespace ActsTests