trout
Phlex-based reconstruction framework for the SHiP experiment.
Loading...
Searching...
No Matches
SpectrometerCkf.hpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2026 CERN for the benefit of the SHiP Collaboration
2//
3// SPDX-License-Identifier: LGPL-3.0-or-later
4
5// SpectrometerCkf.hpp — Acts::CombinatorialKalmanFilter wrapper for the
6// straw-tube spectrometer, replacing ToyKalmanFilter/ToyDetector's
7// single-hand-picked-hit fit with real pattern recognition: seeds are built
8// from every station-1/station-2 hit pair, and the CKF explores all
9// compatible hits at each downstream station (no measurement-selector chi2
10// cut yet — every candidate at a surface is kept, so ambiguity resolution
11// between branches is a later step).
12
13#pragma once
14
15#include "IndexSourceLink.hpp"
17#include "Utilities.hpp"
18
19#include <Acts/EventData/ParticleHypothesis.hpp>
20#include <Acts/EventData/TrackContainer.hpp>
21#include <Acts/EventData/VectorMultiTrajectory.hpp>
22#include <Acts/EventData/VectorTrackContainer.hpp>
23#include <Acts/Geometry/GeometryContext.hpp>
24#include <Acts/Geometry/GeometryIdentifier.hpp>
25#include <Acts/Geometry/TrackingGeometry.hpp>
26#include <Acts/MagneticField/MagneticFieldProvider.hpp>
27#include <Acts/Propagator/EigenStepper.hpp>
28#include <Acts/Propagator/Navigator.hpp>
29#include <Acts/Propagator/Propagator.hpp>
30#include <Acts/TrackFinding/CombinatorialKalmanFilter.hpp>
31#include <Acts/TrackFinding/TrackStateCreator.hpp>
32#include <Acts/TrackFitting/GainMatrixSmoother.hpp>
33#include <Acts/TrackFitting/GainMatrixUpdater.hpp>
34#include <Acts/TrackFitting/KalmanFitter.hpp>
35#include <Acts/Utilities/CalibrationContext.hpp>
36#include <Acts/Utilities/Logger.hpp>
37#include <Acts/Utilities/VectorHelpers.hpp>
38
39#include <SHiP/TrackFitResult.hpp>
40#include <algorithm>
41#include <functional>
42#include <limits>
43#include <memory>
44#include <optional>
45#include <spdlog/spdlog.h>
46#include <utility>
47#include <vector>
48
49namespace SHiP::trout {
50
51// Straight-line seed through two hits on different stations. p0 is a fixed
52// placeholder momentum (no curvature estimate yet — field-off/low-field
53// assumption between the first two stations), same as the previous
54// hand-rolled seed in keep_tracks.cpp.
55inline std::optional<Acts::BoundTrackParameters> makeSeedFromHitPair(
58 Acts::Vector3 const direction = (m1.global - m0.global).normalized();
59 if (!direction.allFinite())
60 return std::nullopt;
61
62 Acts::BoundVector pars = Acts::BoundVector::Zero();
63 pars[Acts::eBoundLoc0] = m0.local[0];
64 pars[Acts::eBoundLoc1] = m0.local[1];
65 pars[Acts::eBoundTime] = 0.;
66 pars[Acts::eBoundPhi] = Acts::VectorHelpers::phi(direction);
67 pars[Acts::eBoundTheta] = Acts::VectorHelpers::theta(direction);
68 double const p0 = 10.0 * Acts::UnitConstants::GeV;
69 pars[Acts::eBoundQOverP] = 1.0 / p0;
70
71 // Widened by ~5.5x (sigma) from the original guess (sLoc=5mm, sAng=0.05,
72 // sQop=0.5/p0): with the original values, refit chi2/ndf on real 4-hit
73 // tracks (as opposed to CKF's occasional wrong/combinatorial 4-hit
74 // matches, distinguishable by a far larger chi2) was running ~900,
75 // implying the assumed sigma was too tight by about sqrt(900/1) ~ 30x in
76 // chi2, i.e. ~5.5x in sigma. This won't make wrong matches look good too
77 // (chi2 ~ 1/sigma^2, so they just move from absurd to merely bad), so a
78 // chi2/ndf cut downstream can still separate real tracks from those.
79 Acts::BoundMatrix cov = Acts::BoundMatrix::Zero();
80 double const sLoc = 27.5 * Acts::UnitConstants::mm;
81 cov(Acts::eBoundLoc0, Acts::eBoundLoc0) = sLoc * sLoc;
82 cov(Acts::eBoundLoc1, Acts::eBoundLoc1) = sLoc * sLoc;
83 double const sT = 1.0 * Acts::UnitConstants::ns;
84 cov(Acts::eBoundTime, Acts::eBoundTime) = sT * sT;
85 double const sAng = 0.275;
86 cov(Acts::eBoundPhi, Acts::eBoundPhi) = sAng * sAng;
87 cov(Acts::eBoundTheta, Acts::eBoundTheta) = sAng * sAng;
88 double const sQop = 2.75 / p0;
89 cov(Acts::eBoundQOverP, Acts::eBoundQOverP) = sQop * sQop;
90
91 return Acts::BoundTrackParameters(m0.surface->getSharedPtr(), pars, cov,
92 Acts::ParticleHypothesis::pion());
93}
94
95// Cheap stand-in for a real chi2 measurement selector: keeps only the
96// candidate whose calibrated local position is closest (squared distance)
97// to the predicted one at this surface, discarding the rest. With no
98// selector at all, every hit within the (currently generous) station-match
99// window is accepted as a separate branch — hundreds of candidates on a
100// busy station — which is the likely cause of the CKF crash; this collapses
101// that back down to one candidate per surface to test that diagnosis before
102// committing to a real Acts::MeasurementSelector.
103template <typename TrackStateProxy>
104Acts::Result<std::pair<typename std::vector<TrackStateProxy>::iterator,
105 typename std::vector<TrackStateProxy>::iterator>>
106selectNearestMeasurement(std::vector<TrackStateProxy>& candidates, bool& isOutlier,
107 const Acts::Logger&) {
108 isOutlier = false;
109 if (candidates.empty())
110 return std::pair{candidates.begin(), candidates.end()};
111
112 auto best = candidates.begin();
113 double bestDist2 = std::numeric_limits<double>::max();
114 for (auto it = candidates.begin(); it != candidates.end(); ++it) {
115 auto const pred = it->predicted();
116 auto const meas = it->template calibrated<2>();
117 double const dx = pred[Acts::eBoundLoc0] - meas[0];
118 double const dy = pred[Acts::eBoundLoc1] - meas[1];
119 double const dist2 = dx * dx + dy * dy;
120 if (dist2 < bestDist2) {
121 bestDist2 = dist2;
122 best = it;
123 }
124 }
125 std::iter_swap(candidates.begin(), best);
126 return std::pair{candidates.begin(), std::next(candidates.begin())};
127}
128
130 public:
131 using Trajectory = Acts::VectorMultiTrajectory;
132 using TrackContainerBackend = Acts::VectorTrackContainer;
133 using BField = Acts::MagneticFieldProvider;
134 using Stepper = Acts::EigenStepper<>;
135 using Navigator = Acts::Navigator;
136 using Propagator = Acts::Propagator<Stepper, Navigator>;
137
138 SpectrometerCkf(std::shared_ptr<const Acts::TrackingGeometry> geometry,
139 std::shared_ptr<Acts::MagneticFieldProvider> field)
140 : m_geometry{std::move(geometry)},
141 m_geoContext(Acts::GeometryContext::dangerouslyDefaultConstruct()),
142 m_field{std::move(field)} {
143 Stepper stepper(m_field);
144 Navigator::Config navCfg;
145 navCfg.trackingGeometry = m_geometry;
146 Navigator navigator(navCfg, Acts::getDefaultLogger("Navigator", Acts::Logging::INFO));
147
148 Propagator propagator(std::move(stepper), std::move(navigator));
149 m_ckf = std::make_unique<Ckf>(std::move(propagator),
150 Acts::getDefaultLogger("CKF", Acts::Logging::INFO));
151
152 // Separate stepper/navigator/propagator for the refit() KalmanFitter
153 // below — the CKF's own propagator was just moved into m_ckf.
154 Stepper fitStepper(m_field);
155 Navigator::Config fitNavCfg;
156 fitNavCfg.trackingGeometry = m_geometry;
157 Navigator fitNavigator(fitNavCfg,
158 Acts::getDefaultLogger("FitNavigator", Acts::Logging::INFO));
159 Propagator fitPropagator(std::move(fitStepper), std::move(fitNavigator));
160 m_fitter = std::make_unique<Fitter>(std::move(fitPropagator));
161 }
162
163 // Runs the CKF from one seed and returns the ordered measurement indices
164 // each surviving candidate branch picked up. A TrackProxy can't outlive
165 // the TrackContainer built inside this call, so hit indices are the only
166 // thing that can cross out to a caller — pass them to refit() below to
167 // get a real fit for one candidate.
168 std::vector<std::vector<ActsExamples::Index>> findCandidateHits(
169 Acts::BoundTrackParameters const& seed,
170 SpectrometerMeasurements const& measurements) const {
171 std::vector<std::vector<ActsExamples::Index>> out;
172
173 IndexSourceLinkAccessor sourceLinkAccessor;
174 sourceLinkAccessor.container = &measurements.sourceLinks();
175
176 using TrackStateCreatorType =
177 Acts::TrackStateCreator<IndexSourceLinkAccessor::Iterator, TrackContainer>;
178 TrackStateCreatorType trackStateCreator;
179 trackStateCreator.sourceLinkAccessor.template connect<&IndexSourceLinkAccessor::range>(
180 &sourceLinkAccessor);
181 trackStateCreator.calibrator
182 .template connect<&SpectrometerMeasurements::calibrator<Trajectory>>(&measurements);
183 // Cheap nearest-candidate cut (see selectNearestMeasurement above) in
184 // place of the default accept-everything selector — unbounded
185 // branching on busy stations is otherwise combinatorially explosive.
186 trackStateCreator.measurementSelector.template connect<
188
189 Acts::GainMatrixUpdater kfUpdater;
190
191 Acts::CombinatorialKalmanFilterExtensions<TrackContainer> extensions;
192 extensions.updater.template connect<&Acts::GainMatrixUpdater::operator()<Trajectory>>(
193 &kfUpdater);
194 extensions.createTrackStates.template connect<&TrackStateCreatorType::createTrackStates>(
195 &trackStateCreator);
196
197 Acts::PropagatorPlainOptions propOptions(m_geoContext, m_magContext);
198 // Bound legitimate integration through the real field over the ~11 m
199 // station span, which can need more than a handful of adaptive steps.
200 propOptions.maxSteps = 500;
201 propOptions.pathLimit = 50000. * Acts::UnitConstants::mm; // stations span ~11 m
202
203 Acts::CombinatorialKalmanFilterOptions<TrackContainer> options(
204 m_geoContext, m_magContext, std::cref(m_calibContext), extensions, propOptions);
205
206 TrackContainerBackend trackStorage;
207 Trajectory trajStorage;
208 TrackContainer tracks(trackStorage, trajStorage);
209
210 auto result = m_ckf->findTracks(seed, options, tracks);
211 if (!result.ok()) {
212 spdlog::warn("CKF track finding failed: {}", result.error().message());
213 return out;
214 }
215 for (auto const& trackProxy : *result) {
216 // Branches that never got extended with a measurement (no
217 // branchStopper is wired up, so the CKF can still hand these back)
218 // have no track states at all — skip them rather than crash
219 // walking an invalid tip.
220 if (trackProxy.tipIndex() == Acts::kTrackIndexInvalid)
221 continue;
222 std::vector<ActsExamples::Index> hits;
223 tracks.trackStateContainer().visitBackwards(trackProxy.tipIndex(), [&](auto ts) {
224 if (!ts.typeFlags().isMeasurement())
225 return true;
226 // isMeasurement() alone doesn't guarantee this — ACTS itself only
227 // asserts it inside getUncalibratedSourceLink() (stripped by our
228 // -DNDEBUG release build), so an unguarded call here threw
229 // bad_optional_access until SpectrometerMeasurements::calibrator
230 // was fixed to actually set it.
231 if (!ts.hasUncalibratedSourceLink())
232 return true;
233 // getUncalibratedSourceLink() returns the SourceLink by
234 // value — copy the payload out rather than binding a
235 // reference into the temporary.
236 auto const isl = ts.getUncalibratedSourceLink().template get<IndexSourceLink>();
237 hits.push_back(isl.index());
238 return true;
239 });
240 if (!hits.empty())
241 out.push_back(std::move(hits));
242 }
243 return out;
244 }
245
246 // Refits one candidate's hit list with a real forward+backward-smoothed
247 // Acts::KalmanFitter, starting from the same straight-line `seed` used to
248 // find it. Unlike the CKF above (no smoother available — see
249 // trackProxyToFitResult's comment), this gets real smoothed residuals, via
250 // the same fromActsFitResult converter the pre-CKF single-hit fit
251 // used (see ToyKalmanFitter::fit) — appropriate here since a smoother and
252 // reference surface are both actually configured below.
253 SHiP::TrackFitResult refit(Acts::BoundTrackParameters const& seed,
254 SpectrometerMeasurements const& measurements,
255 std::vector<ActsExamples::Index> const& hitIndices) const {
256 // KalmanFitter's regular-Navigator fit() matches a visited surface to a
257 // measurement by raw Surface* pointer equality (see its Actor's
258 // `inputMeasurements.find(&surface)`), built upfront from whatever this
259 // accessor returns per SourceLink. IndexSourceLinkSurfaceAccessor would
260 // re-derive that pointer via TrackingGeometry::findSurface(geometryId) —
261 // a different lookup path than what the Navigator itself hands back
262 // while crossing the surface, which this geometry backend has already
263 // shown (the earlier Gen1/Gen3 portal issue) isn't always pointer-stable
264 // by id alone. Sidestep that entirely: SpectrometerMeasurements already
265 // captured the exact Surface* each hit was built against, so use that
266 // directly instead of re-deriving anything.
267 struct MeasurementSurfaceAccessor {
268 SpectrometerMeasurements const* measurements;
269 const Acts::Surface* operator()(Acts::SourceLink const& sl) const {
270 auto const& isl = sl.get<IndexSourceLink>();
271 return measurements->measurements()[isl.index()].surface;
272 }
273 };
274
275 Acts::GainMatrixUpdater kfUpdater;
276 Acts::GainMatrixSmoother kfSmoother;
277 MeasurementSurfaceAccessor surfaceAccessor{&measurements};
278
279 Acts::KalmanFitterExtensions<Trajectory> ext;
280 ext.surfaceAccessor.template connect<&MeasurementSurfaceAccessor::operator()>(
281 &surfaceAccessor);
282 ext.calibrator.template connect<&SpectrometerMeasurements::calibrator<Trajectory>>(
283 &measurements);
284 ext.updater.template connect<&Acts::GainMatrixUpdater::operator()<Trajectory>>(&kfUpdater);
285 ext.smoother.template connect<&Acts::GainMatrixSmoother::operator()<Trajectory>>(
286 &kfSmoother);
287
288 Acts::PropagatorPlainOptions propOptions(m_geoContext, m_magContext);
289 propOptions.maxSteps = 500;
290 propOptions.pathLimit = 50000. * Acts::UnitConstants::mm;
291
292 Acts::KalmanFitterOptions<Trajectory> options(m_geoContext, m_magContext,
293 std::cref(m_calibContext), ext, propOptions,
294 &seed.referenceSurface());
295
296 Acts::VectorTrackContainer trackStorage;
297 Trajectory trajStorage;
298 Acts::TrackContainer tracks(std::move(trackStorage), std::move(trajStorage));
299
300 std::vector<Acts::SourceLink> sourceLinks;
301 sourceLinks.reserve(hitIndices.size());
302 for (auto const idx : hitIndices) {
303 auto const& m = measurements.measurements()[idx];
304 sourceLinks.emplace_back(IndexSourceLink{m.surface->geometryId(), idx});
305 }
306
307 auto result = m_fitter->fit(sourceLinks.begin(), sourceLinks.end(), seed, options, tracks);
308 return fromActsFitResult(result, tracks, m_geoContext);
309 }
310
311 private:
312 using TrackContainer = Acts::TrackContainer<TrackContainerBackend, Trajectory>;
313 using Ckf = Acts::CombinatorialKalmanFilter<Propagator, TrackContainer>;
314 using Fitter = Acts::KalmanFitter<Propagator, Trajectory>;
315
316 std::shared_ptr<const Acts::TrackingGeometry> m_geometry;
317 Acts::GeometryContext m_geoContext;
318 Acts::MagneticFieldContext m_magContext;
319 Acts::CalibrationContext m_calibContext;
320 std::shared_ptr<BField> m_field;
321 std::unique_ptr<Ckf> m_ckf;
322 std::unique_ptr<Fitter> m_fitter;
323};
324
325} // namespace SHiP::trout
SHiP::TrackFitResult refit(Acts::BoundTrackParameters const &seed, SpectrometerMeasurements const &measurements, std::vector< ActsExamples::Index > const &hitIndices) const
Definition SpectrometerCkf.hpp:253
Acts::MagneticFieldProvider BField
Definition SpectrometerCkf.hpp:133
SpectrometerCkf(std::shared_ptr< const Acts::TrackingGeometry > geometry, std::shared_ptr< Acts::MagneticFieldProvider > field)
Definition SpectrometerCkf.hpp:138
Acts::VectorTrackContainer TrackContainerBackend
Definition SpectrometerCkf.hpp:132
std::vector< std::vector< ActsExamples::Index > > findCandidateHits(Acts::BoundTrackParameters const &seed, SpectrometerMeasurements const &measurements) const
Definition SpectrometerCkf.hpp:168
Acts::EigenStepper<> Stepper
Definition SpectrometerCkf.hpp:134
Acts::VectorMultiTrajectory Trajectory
Definition SpectrometerCkf.hpp:131
Acts::Navigator Navigator
Definition SpectrometerCkf.hpp:135
Acts::Propagator< Stepper, Navigator > Propagator
Definition SpectrometerCkf.hpp:136
Definition SpectrometerMeasurements.hpp:37
const ActsExamples::GeometryIdMultiset< IndexSourceLink > & sourceLinks() const
Definition SpectrometerMeasurements.hpp:91
const std::vector< Measurement > & measurements() const
Definition SpectrometerMeasurements.hpp:89
Definition fit_seed.cpp:16
Acts::Result< std::pair< typename std::vector< TrackStateProxy >::iterator, typename std::vector< TrackStateProxy >::iterator > > selectNearestMeasurement(std::vector< TrackStateProxy > &candidates, bool &isOutlier, const Acts::Logger &)
Definition SpectrometerCkf.hpp:106
TrackFitResult fromActsFitResult(const Acts::Result< Acts::TrackContainer< Acts::VectorTrackContainer, Acts::VectorMultiTrajectory, Acts::ValueHolder >::TrackProxy > &actsResult, Acts::TrackContainer< Acts::VectorTrackContainer, Acts::VectorMultiTrajectory, Acts::ValueHolder > &tracks, Acts::GeometryContext const &gctx)
Definition Utilities.hpp:20
std::optional< Acts::BoundTrackParameters > makeSeedFromHitPair(const SpectrometerMeasurements::Measurement &m0, const SpectrometerMeasurements::Measurement &m1)
Definition SpectrometerCkf.hpp:55
const Container * container
Definition GeometryContainers.hpp:149
Accessor for the above source link container.
Definition IndexSourceLink.hpp:75
Definition SpectrometerMeasurements.hpp:39
const Acts::Surface * surface
Definition SpectrometerMeasurements.hpp:42
Acts::Vector2 local
Definition SpectrometerMeasurements.hpp:44
Acts::Vector3 global
Definition SpectrometerMeasurements.hpp:43