Project
Loading...
Searching...
No Matches
testCovarianceSanitization.cxx
Go to the documentation of this file.
1// Copyright 2019-2020 CERN and copyright holders of ALICE O2.
2// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders.
3// All rights not expressly granted are reserved.
4//
5// This software is distributed under the terms of the GNU General Public
6// License v3 (GPL Version 3), copied verbatim in the file "COPYING".
7//
8// In applying this license CERN does not waive the privileges and immunities
9// granted to it by virtue of its status as an Intergovernmental Organization
10// or submit itself to any jurisdiction.
11
12// M5d covariance-validity correction (doc/decisions/0008-native-refit-activation.md,
13// covariance-fault-localization investigation): focused, deterministic
14// regression coverage for sanitizeCovariance() (SurfaceTrackState.h) and
15// its eight call sites (barrel rotate/propagate x2 overloads/update, forward
16// propagation x2 overloads/update). Several fixtures below reproduce a
17// real captured production failure verbatim (exact state/covariance/
18// measurement values from a checksummed replay of the
19// pp-20ev-run303000-seed20260716-daily20260717 fixture, candidate keys
20// "13,6,6,5,4,9,5" (ITS) and "68,71,73,67,72,73,62,76,80,-1" (MFT)) rather
21// than a synthetic approximation, per the covariance-fault-localization
22// investigation's minimal-reproducer design.
23
24#define BOOST_TEST_MODULE ITSMFTCovarianceSanitization
25#define BOOST_TEST_MAIN
26#define BOOST_TEST_DYN_LINK
27#include <boost/test/unit_test.hpp>
28
29#include <algorithm>
30#include <cmath>
31#include <cstdint>
32#include <cstring>
33#include <limits>
34
39
40namespace
41{
42using namespace o2::itsmft::tracking;
43
44bool allDiagonalsNonNegative(const SurfaceTrackState& state)
45{
46 for (uint8_t i = 0; i < 5; ++i) {
47 if (state.covariance[packedCovarianceIndex(i, i)] < 0.f) {
48 return false;
49 }
50 }
51 return true;
52}
53
54// Returns the magnitude of the worst pairwise-correlation violation found
55// (0 if none), i.e. max(0, |c_ij|/sqrt(c_ii*c_jj) - 1) over every off-diagonal
56// pair. Callers with a non-negative diagonal already established can compare
57// this against a small float tolerance.
58float maxCorrelationViolation(const SurfaceTrackState& state)
59{
60 float worst = 0.f;
61 for (uint8_t i = 0; i < 5; ++i) {
62 for (uint8_t j = 0; j < i; ++j) {
63 const float dii = state.covariance[packedCovarianceIndex(i, i)];
64 const float djj = state.covariance[packedCovarianceIndex(j, j)];
65 if (dii <= 0.f || djj <= 0.f) {
66 continue;
67 }
68 const float rho = state.covariance[packedCovarianceIndex(i, j)] / std::sqrt(dii * djj);
69 worst = std::max(worst, std::abs(rho) - 1.f);
70 }
71 }
72 return worst;
73}
74
75// The DECLARED invariant sanitizeCovariance() (SurfaceTrackState.h)
76// establishes -- non-negative diagonals and no individual pairwise
77// correlation exceeding unity -- and nothing more. This is deliberately NOT
78// a full positive-semi-definite check (that would additionally require,
79// e.g., every leading principal minor non-negative / every eigenvalue
80// non-negative): the doc comment on sanitizeCovariance() proves with a real
81// captured counter-example that pairwise-valid does not imply full PSD, and
82// this codebase does not claim otherwise. A test asserting full PSD here
83// would be testing an invariant the production code does not establish.
84bool covarianceSatisfiesDeclaredInvariant(const SurfaceTrackState& state, float tolerance = 1.e-3f)
85{
86 return allDiagonalsNonNegative(state) && maxCorrelationViolation(state) <= tolerance;
87}
88
89bool closeTo(float a, float b, float absTol = 5.e-4f, float relTol = 2.e-3f)
90{
91 const float diff = std::fabs(a - b);
92 return diff <= absTol || diff <= relTol * std::fabs(b);
93}
94
95template <typename T>
96bool bitEqual(const T& lhs, const T& rhs)
97{
98 return std::memcmp(&lhs, &rhs, sizeof(T)) == 0;
99}
100
101} // namespace
102
103// --- 1. sanitizeCovariance() itself: the core rule, in isolation. ----------
104
105BOOST_AUTO_TEST_CASE(SanitizeCovarianceAbsNegativeDiagonal)
106{
108 state.covariance[packedCovarianceIndex(0, 0)] = -0.25f;
109 state.covariance[packedCovarianceIndex(1, 1)] = 0.5f;
110 state.covariance[packedCovarianceIndex(2, 2)] = 0.5f;
111 state.covariance[packedCovarianceIndex(3, 3)] = 0.5f;
112 state.covariance[packedCovarianceIndex(4, 4)] = 0.5f;
113 const float maxDiagonal[5] = {1.f, 1.f, 1.f, 1.f, 1.f};
114 sanitizeCovariance(state, maxDiagonal);
115 BOOST_CHECK_CLOSE(state.covariance[packedCovarianceIndex(0, 0)], 0.25f, 1e-4f);
116 BOOST_CHECK(allDiagonalsNonNegative(state));
117}
118
119BOOST_AUTO_TEST_CASE(SanitizeCovarianceClampsOverRangeAndRescalesOffDiagonal)
120{
121 // Pass 2 (pairwise correlation clamp) runs after pass 1 and would itself
122 // touch an off-diagonal whose implied correlation, computed from the
123 // POST-pass-1 (already range-clamped) diagonals, still exceeds 1 -- so
124 // this fixture is deliberately chosen so pass 1's own rescale already
125 // brings every off-diagonal within pass 2's bound too, isolating pass 1
126 // in observable behavior (SanitizeCovarianceClampsOverRangeToCauchySchwarzBound
127 // below exercises pass 2 specifically, including its interaction with an
128 // already-pass-1-clamped diagonal).
130 state.covariance[packedCovarianceIndex(0, 0)] = 4.f; // 4x the max below.
131 state.covariance[packedCovarianceIndex(1, 0)] = 1.f; // Shares row/column 0.
132 state.covariance[packedCovarianceIndex(2, 0)] = 0.4f;
133 state.covariance[packedCovarianceIndex(1, 1)] = 0.3f;
134 state.covariance[packedCovarianceIndex(2, 2)] = 0.3f;
135 state.covariance[packedCovarianceIndex(3, 3)] = 0.3f;
136 state.covariance[packedCovarianceIndex(4, 4)] = 0.3f;
137 const float maxDiagonal[5] = {1.f, 1.f, 1.f, 1.f, 1.f};
138 sanitizeCovariance(state, maxDiagonal);
139 // scale = sqrt(max/old) = sqrt(1/4) = 0.5.
140 BOOST_CHECK_CLOSE(state.covariance[packedCovarianceIndex(0, 0)], 1.f, 1e-4f);
141 BOOST_CHECK_CLOSE(state.covariance[packedCovarianceIndex(1, 0)], 0.5f, 1e-4f);
142 BOOST_CHECK_CLOSE(state.covariance[packedCovarianceIndex(2, 0)], 0.2f, 1e-4f);
143 // Untouched entries not sharing the clamped row/column.
144 BOOST_CHECK_CLOSE(state.covariance[packedCovarianceIndex(1, 1)], 0.3f, 1e-4f);
145 // Confirms pass 2 really was a no-op for this fixture, not merely unlucky
146 // arithmetic: every pairwise correlation is within bound.
147 BOOST_CHECK_LE(maxCorrelationViolation(state), 1.e-4f);
148}
149
150BOOST_AUTO_TEST_CASE(SanitizeCovarianceClampsOverRangeToCauchySchwarzBound)
151{
152 // Pass 2 in isolation (diagonals already within maxDiagonal, so pass 1 is
153 // a no-op here): an off-diagonal whose magnitude implies |correlation|>1
154 // is clamped to exactly sqrt(c_ii*c_jj), sign preserved; a pair already
155 // within bound is untouched.
157 state.covariance[packedCovarianceIndex(0, 0)] = 4.f;
158 state.covariance[packedCovarianceIndex(1, 1)] = 9.f;
159 state.covariance[packedCovarianceIndex(1, 0)] = -100.f; // |rho| = 100/sqrt(4*9) = 16.67, deliberately over 1.
160 state.covariance[packedCovarianceIndex(2, 2)] = 4.f;
161 state.covariance[packedCovarianceIndex(2, 0)] = 3.f; // |rho| = 3/sqrt(4*4) = 0.75, already within bound.
162 state.covariance[packedCovarianceIndex(3, 3)] = 1.f;
163 state.covariance[packedCovarianceIndex(4, 4)] = 1.f;
164 const float maxDiagonal[5] = {1.e30f, 1.e30f, 1.e30f, 1.e30f, 1.e30f}; // Effectively unreachable: isolates pass 2.
165 sanitizeCovariance(state, maxDiagonal);
166 BOOST_CHECK_CLOSE(state.covariance[packedCovarianceIndex(1, 0)], -6.f, 1e-4f); // -sqrt(4*9) = -6.
167 BOOST_CHECK_CLOSE(state.covariance[packedCovarianceIndex(2, 0)], 3.f, 1e-4f); // Untouched: already within bound.
168 BOOST_CHECK_LE(maxCorrelationViolation(state), 1.e-4f);
169}
170
171BOOST_AUTO_TEST_CASE(SanitizeCovariancePreservesSymmetryByConstruction)
172{
173 // Packed lower-triangular storage: only one entry exists per (row,column)
174 // pair, so "symmetry" is a representation invariant, not a check --
175 // packedCovarianceIndex(i,j) == packedCovarianceIndex(j,i) is exercised
176 // directly by every read/write sanitizeCovariance performs. Diagonals are
177 // set generously large (relative to the off-diagonal under test) so pass
178 // 2's correlation clamp is a no-op here and does not confound the
179 // symmetry check with a legitimate clamp.
181 state.covariance[packedCovarianceIndex(1, 1)] = 100.f;
182 state.covariance[packedCovarianceIndex(3, 3)] = 100.f;
183 state.covariance[packedCovarianceIndex(3, 1)] = 5.f;
184 const float maxDiagonal[5] = {1.e30f, 1.e30f, 1.e30f, 1.e30f, 1.e30f};
185 sanitizeCovariance(state, maxDiagonal);
186 BOOST_CHECK_EQUAL(packedCovarianceIndex(1, 3), packedCovarianceIndex(3, 1));
187 BOOST_CHECK_CLOSE(state.covariance[packedCovarianceIndex(1, 3)], 5.f, 1e-4f);
188}
189
190// --- 2. ITS legB reproducer: Propagator::updateBarrel() on the exact captured real --
191// prior state/covariance and measurement (candidate "13,6,6,5,4,9,5", hit 5)
192// that failed material correction because of an invalid covariance (posterior
193// Q2Pt-Q2Pt diagonal = -0.032802999, real production value, captured
194// verbatim from the checksummed 20-event replay).
195
196BOOST_AUTO_TEST_CASE(ITSLegBReproducerNowSanitizesToValidCovariance)
197{
199 state.kind = SurfaceKind::Cylinder;
200 state.referenceCoordinate = 3.76323366f;
201 state.alpha = -0.12901926f;
202 state.parameters[0] = 0.642236829f;
203 state.parameters[1] = -6.11814785f;
204 state.parameters[2] = 0.167980343f;
205 state.parameters[3] = -1.58871007f;
206 state.parameters[4] = 1.2842629f;
207 state.absCharge = 1;
208 state.pid = o2::track::PID::Pion;
209 const float cov[15] = {
210 0.0615117364f, -0.0162716303f, 0.00781002454f, -0.00648284703f, 0.00164899346f, 0.000680086901f,
211 0.000152464694f, -0.000262976653f, -1.178005e-05f, 1.45164713e-05f,
212 -0.22814776f, 0.0546577908f, 0.0237723477f, -0.000194984852f, 0.822642863f};
213 for (int i = 0; i < 15; ++i) {
214 state.covariance[i] = cov[i];
215 }
216
217 SurfaceMeasurement meas{};
218 meas.frame.u = 0.633100867f;
219 meas.frame.v = -6.10807085f;
220 meas.covariance.uu = 1.18710993e-07f;
221 meas.covariance.uv = 0.f;
222 meas.covariance.vv = 3.60069805e-07f;
223
224 float chi2 = 0.f;
225
226 const bool ok = Propagator::updateBarrel(state, meas, chi2);
227
228 BOOST_REQUIRE(ok);
229 BOOST_CHECK(allDiagonalsNonNegative(state));
230 BOOST_CHECK_CLOSE(state.covariance[packedCovarianceIndex(4, 4)], 0.0328048468f, 5.f); // sign-flipped, matches production magnitude within float tolerance.
231}
232
233// --- 3. MFT reproducer: Propagator::updateForward() on the exact captured real ------
234// prior state/covariance and measurement (candidate
235// "68,71,73,67,72,73,62,76,80,-1", legB, hit 3) that produced a
236// Q2Pt-Q2Pt diagonal of -52.064167 (real production value) before this
237// correction.
238
239BOOST_AUTO_TEST_CASE(MFTReproducerNowSanitizesToValidCovariance)
240{
242 state.kind = SurfaceKind::Disk;
243 state.referenceCoordinate = -67.6889038f;
244 state.alpha = 0.f;
245 state.parameters[0] = -3.40663648f;
246 state.parameters[1] = -3.04799104f;
247 state.parameters[2] = -2.40926218f;
248 state.parameters[3] = -15.2632132f;
249 state.parameters[4] = -0.0805783421f;
250 state.absCharge = 1;
251 state.pid = o2::track::PID::Pion;
252 const float cov[15] = {
253 5.45968469e-05f, 7.92145147e-05f, 2.34508334e-05f, 0.000361069426f, 9.60996113e-05f, 0.00121101411f,
254 0.00140110496f, 0.0018511567f, 0.00489055132f, 0.0514357649f,
255 0.117691882f, 0.0217776336f, 0.452787817f, 1.21933973f, 168.588654f};
256 for (int i = 0; i < 15; ++i) {
257 state.covariance[i] = cov[i];
258 }
259
260 SurfaceMeasurement meas{};
261 meas.frame.u = -3.4059999f;
262 meas.frame.v = -3.04678011f;
263 meas.covariance.uu = 4.4239976e-05f;
264 meas.covariance.uv = 0.f;
265 meas.covariance.vv = 0.000105412393f;
266
267 float chi2 = 0.f;
268
269 const bool ok = Propagator::updateForward(state, meas, chi2);
270
271 BOOST_REQUIRE(ok);
272 BOOST_CHECK(allDiagonalsNonNegative(state));
273}
274
275// --- 4. Large-step propagation invariant: Propagator::propagateBarrel(state, linRef, --
276// ...) on the exact captured real inputs that fed the ITS legB reproducer
277// above (the immediately preceding hit) must itself leave the covariance
278// invariant satisfied before the next update() ever runs. The raw off-
279// diagonal transport for this large (~-15.5cm) step makes THREE pairwise
280// correlations simultaneously exceed 1 in magnitude -- (Y,Snp), (Y,Q2Pt),
281// (Snp,Q2Pt) -- confirmed against the real captured (pre-correction)
282// production values: c(Y,Y)=0.0615117364, c(Y,Q2Pt)=-0.22814776,
283// c(Q2Pt,Q2Pt)=0.822642863 give rho(Y,Q2Pt) = -0.22814776 /
284// sqrt(0.0615117364*0.822642863) = -1.0142..., i.e. |rho|>1 while every
285// diagonal individually stays positive and unremarkable -- exactly the
286// precondition the covariance-fault-localization investigation traced.
287// sanitizeCovariance()'s pass 2 must repair all three before this function
288// returns, and the immediately following measurement update (same real
289// captured measurement) must then observe the DECLARED invariant on its
290// own committed output too -- not merely "not obviously wrong": pass 2
291// alone measurably shrinks (from -0.0328 to a much smaller magnitude) but
292// does not eliminate the negative diagonal the update's own naive Kalman
293// subtraction still produces from an otherwise-repaired input (see
294// sanitizeCovariance()'s own doc comment for the full empirical accounting
295// of this), so pass 1 (diagonal abs) remains load-bearing for the
296// observable, committed result even with pass 2 active.
297BOOST_AUTO_TEST_CASE(LargeStepPropagationRepairsCorrelationBeforeUpdate)
298{
300 state.kind = SurfaceKind::Cylinder;
301 state.referenceCoordinate = 19.2192478f;
302 state.alpha = -0.12901926f;
303 state.parameters[0] = 3.03678966f;
304 state.parameters[1] = -30.9622726f;
305 state.parameters[2] = 0.138186395f;
306 state.parameters[3] = -1.58871007f;
307 state.parameters[4] = 1.2842629f;
308 state.absCharge = 1;
309 state.pid = o2::track::PID::Pion;
310 const float cov[15] = {
311 1.98605832e-07f, -7.50241043e-08f, 2.4906555e-07f, -9.13163856e-09f, -3.06560999e-08f, 1.98362322e-05f,
312 3.80933152e-09f, -3.28565477e-08f, -7.25654581e-06f, 1.45164713e-05f,
313 -1.76052566e-07f, -8.04973183e-07f, 0.00468764221f, -0.000194984852f, 0.822642863f};
314 for (int i = 0; i < 15; ++i) {
315 state.covariance[i] = cov[i];
316 }
317
318 SurfaceTrackParameters linRef{};
319 linRef.kind = SurfaceKind::Cylinder;
320 linRef.referenceCoordinate = 19.2192478f;
321 linRef.alpha = -0.12901926f;
322 linRef.parameters[0] = 3.03678894f;
323 linRef.parameters[1] = -30.962265f;
324 linRef.parameters[2] = 0.137899101f;
325 linRef.parameters[3] = -1.58717895f;
326 linRef.parameters[4] = 1.21498108f;
327
328 const float targetX = 3.76323366f;
329 const float bz = 5.00675011f;
330
331 const bool ok = Propagator::propagateBarrel(state, linRef, targetX, bz);
332
333 BOOST_REQUIRE(ok);
334 BOOST_CHECK(covarianceSatisfiesDeclaredInvariant(state));
335 // Diagonals themselves are untouched by pass 2 (only off-diagonals move):
336 // still match the real captured production values exactly.
337 BOOST_CHECK(closeTo(state.covariance[packedCovarianceIndex(0, 0)], 0.0615117364f));
338 BOOST_CHECK(closeTo(state.covariance[packedCovarianceIndex(4, 4)], 0.822642863f));
339 // The (Y,Q2Pt) pair is now repaired to exactly touch (not exceed) the
340 // Cauchy-Schwarz bound, rather than the real pre-correction production
341 // value of -0.22814776 (|rho|=1.0142).
342 const float expectedC40 = -std::sqrt(state.covariance[packedCovarianceIndex(0, 0)] * state.covariance[packedCovarianceIndex(4, 4)]);
343 BOOST_CHECK(closeTo(state.covariance[packedCovarianceIndex(4, 0)], expectedC40));
344 BOOST_CHECK_LE(maxCorrelationViolation(state), 1.e-3f);
345
346 // The following update (same real captured measurement) must observe the
347 // declared invariant on its own committed output.
348 SurfaceMeasurement meas{};
349 meas.frame.u = 0.633100867f;
350 meas.frame.v = -6.10807085f;
351 meas.covariance.uu = 1.18710993e-07f;
352 meas.covariance.uv = 0.f;
353 meas.covariance.vv = 3.60069805e-07f;
354 float chi2 = 0.f;
355
356 BOOST_REQUIRE(Propagator::updateBarrel(state, meas, chi2));
357 BOOST_CHECK(covarianceSatisfiesDeclaredInvariant(state));
358}
359
360// --- 5. Every rotate/propagate/update independently sanitizes, both -------
361// families. Each case below uses a deliberate zero-step (rotate: delta==0;
362// propagate: dx/dz==0) or an otherwise-trivial transport so the operation's
363// own transform is a documented no-op/identity on the covariance, isolating
364// the sanitization call itself as the only thing that can explain a clamped
365// result -- rather than depending on a from-scratch derivation of each
366// operation's own Jacobian to predict a non-trivial expected output.
367
369{
371 state.kind = SurfaceKind::Cylinder;
373 state.alpha = 0.3f;
374 state.parameters[0] = 1.25f;
375 state.parameters[1] = -0.75f;
376 state.parameters[2] = 0.2f;
377 state.parameters[3] = -0.35f;
378 state.parameters[4] = 0.05f; // Small |Q2Pt| so Q2Pt-Q2Pt max isn't reached trivially by other tests.
379 state.absCharge = 1;
380 state.pid = o2::track::PID::Pion;
381 state.covariance[packedCovarianceIndex(0, 0)] = 50.f * o2::track::kCY2max; // Deliberately over range.
382 state.covariance[packedCovarianceIndex(1, 1)] = 0.01f;
383 state.covariance[packedCovarianceIndex(2, 2)] = 0.01f;
384 state.covariance[packedCovarianceIndex(3, 3)] = 0.01f;
385 state.covariance[packedCovarianceIndex(4, 4)] = 0.01f;
386 return state;
387}
388
389BOOST_AUTO_TEST_CASE(BarrelRotateSanitizesOnZeroDeltaTrivialStep)
390{
392
393 const bool ok = Propagator::rotateBarrel(state, state.alpha); // delta == 0: ratio == 1, transform is identity.
394 BOOST_REQUIRE(ok);
395 BOOST_CHECK_CLOSE(state.covariance[packedCovarianceIndex(0, 0)], o2::track::kCY2max, 1e-3f);
396}
397
398BOOST_AUTO_TEST_CASE(BarrelPropagateSanitizesOnZeroDxTrivialStep)
399{
401
402 const bool ok = Propagator::propagateBarrel(state, state.referenceCoordinate, 0.5f); // dx == 0: early-return path.
403 BOOST_REQUIRE(ok);
404 BOOST_CHECK_CLOSE(state.covariance[packedCovarianceIndex(0, 0)], o2::track::kCY2max, 1e-3f);
405}
406
407BOOST_AUTO_TEST_CASE(BarrelUpdateSanitizesReproducer)
408{
409 // Same fixture and assertion as ITSLegBReproducerNowSanitizesToValidCovariance
410 // above; kept as a separate, minimally-named case so "update sanitizes" is
411 // independently visible in the test list without relying on the reproducer
412 // test's name to convey it.
414 state.kind = SurfaceKind::Cylinder;
415 state.referenceCoordinate = 3.76323366f;
416 state.alpha = -0.12901926f;
417 state.parameters[0] = 0.642236829f;
418 state.parameters[1] = -6.11814785f;
419 state.parameters[2] = 0.167980343f;
420 state.parameters[3] = -1.58871007f;
421 state.parameters[4] = 1.2842629f;
422 state.absCharge = 1;
423 state.pid = o2::track::PID::Pion;
424 const float cov[15] = {
425 0.0615117364f, -0.0162716303f, 0.00781002454f, -0.00648284703f, 0.00164899346f, 0.000680086901f,
426 0.000152464694f, -0.000262976653f, -1.178005e-05f, 1.45164713e-05f,
427 -0.22814776f, 0.0546577908f, 0.0237723477f, -0.000194984852f, 0.822642863f};
428 for (int i = 0; i < 15; ++i) {
429 state.covariance[i] = cov[i];
430 }
431 SurfaceMeasurement meas{};
432 meas.frame.u = 0.633100867f;
433 meas.frame.v = -6.10807085f;
434 meas.covariance.uu = 1.18710993e-07f;
435 meas.covariance.vv = 3.60069805e-07f;
436 float chi2 = 0.f;
437
438 BOOST_REQUIRE(Propagator::updateBarrel(state, meas, chi2));
439 BOOST_CHECK(allDiagonalsNonNegative(state));
440}
441
442BOOST_AUTO_TEST_CASE(BarrelLinRefRotateSanitizesOnZeroDeltaTrivialStep)
443{
445 SurfaceTrackParameters linRef{};
446 linRef.kind = SurfaceKind::Cylinder;
447 linRef.referenceCoordinate = state.referenceCoordinate;
448 linRef.alpha = state.alpha;
449 for (int i = 0; i < 5; ++i) {
450 linRef.parameters[i] = state.parameters[i];
451 }
452
453 const bool ok = Propagator::rotateBarrel(state, linRef, state.alpha, 0.5f);
454 BOOST_REQUIRE(ok);
455 BOOST_CHECK_CLOSE(state.covariance[packedCovarianceIndex(0, 0)], o2::track::kCY2max, 1e-3f);
456}
457
458BOOST_AUTO_TEST_CASE(BarrelLinRefPropagateSanitizesLargeStep)
459{
460 // Same fixture and assertion as LargeStepPropagationPreservesInvariantBeforeUpdate
461 // above; kept as a separate, minimally-named case for the same reason as
462 // BarrelUpdateSanitizesReproducer.
464 state.kind = SurfaceKind::Cylinder;
465 state.referenceCoordinate = 19.2192478f;
466 state.alpha = -0.12901926f;
467 state.parameters[0] = 3.03678966f;
468 state.parameters[1] = -30.9622726f;
469 state.parameters[2] = 0.138186395f;
470 state.parameters[3] = -1.58871007f;
471 state.parameters[4] = 1.2842629f;
472 state.absCharge = 1;
473 state.pid = o2::track::PID::Pion;
474 const float cov[15] = {
475 1.98605832e-07f, -7.50241043e-08f, 2.4906555e-07f, -9.13163856e-09f, -3.06560999e-08f, 1.98362322e-05f,
476 3.80933152e-09f, -3.28565477e-08f, -7.25654581e-06f, 1.45164713e-05f,
477 -1.76052566e-07f, -8.04973183e-07f, 0.00468764221f, -0.000194984852f, 0.822642863f};
478 for (int i = 0; i < 15; ++i) {
479 state.covariance[i] = cov[i];
480 }
481 SurfaceTrackParameters linRef{};
482 linRef.kind = SurfaceKind::Cylinder;
483 linRef.referenceCoordinate = 19.2192478f;
484 linRef.alpha = -0.12901926f;
485 linRef.parameters[0] = 3.03678894f;
486 linRef.parameters[1] = -30.962265f;
487 linRef.parameters[2] = 0.137899101f;
488 linRef.parameters[3] = -1.58717895f;
489 linRef.parameters[4] = 1.21498108f;
490
491 BOOST_REQUIRE(Propagator::propagateBarrel(state, linRef, 3.76323366f, 5.00675011f));
492 BOOST_CHECK(allDiagonalsNonNegative(state));
493}
494
495// Forward has no established diagonal-range validity bound (see
496// kForwardMaxDiagonal's own doc comment, Propagator.cxx:
497// legacy MFT's fitting engine has no covariance-sanitization mechanism at
498// all, so forward's range-clamp sub-pass is deliberately disabled pending a
499// separate design decision), so an over-range diagonal is no longer a valid
500// forward wiring probe. A deliberately over-correlated off-diagonal pair is:
501// the pairwise correlation bound is mathematically universal (Cauchy-
502// Schwarz), not a detector-specific bound, and is fully active for forward.
504{
506 state.kind = SurfaceKind::Disk;
508 state.alpha = 0.f;
509 state.parameters[0] = 1.f;
510 state.parameters[1] = -1.f;
511 state.parameters[2] = 0.1f;
512 state.parameters[3] = -2.f;
513 state.parameters[4] = 0.05f;
514 state.absCharge = 1;
515 state.pid = o2::track::PID::Pion;
516 state.covariance[packedCovarianceIndex(0, 0)] = 4.f;
517 state.covariance[packedCovarianceIndex(1, 0)] = 100.f; // |rho(X,Y)| = 100/sqrt(4*1) = 50, deliberately over 1.
518 state.covariance[packedCovarianceIndex(1, 1)] = 1.f;
519 state.covariance[packedCovarianceIndex(2, 2)] = 0.01f;
520 state.covariance[packedCovarianceIndex(3, 3)] = 0.01f;
521 state.covariance[packedCovarianceIndex(4, 4)] = 0.01f;
522 return state;
523}
524
525BOOST_AUTO_TEST_CASE(ForwardPropagateSanitizesOnZeroDzTrivialStep)
526{
528
529 const bool ok = Propagator::propagateToReference(state, state.referenceCoordinate, 0.5f);
530 BOOST_REQUIRE(ok);
531 BOOST_CHECK(covarianceSatisfiesDeclaredInvariant(state));
532 BOOST_CHECK_CLOSE(state.covariance[packedCovarianceIndex(1, 0)], 2.f, 1e-3f); // sqrt(4*1) = 2, sign-preserved.
533}
534
535BOOST_AUTO_TEST_CASE(ForwardLinRefPropagateSanitizesOnZeroDzTrivialStep)
536{
538 SurfaceTrackParameters linRef{};
539 linRef.kind = SurfaceKind::Disk;
540 linRef.referenceCoordinate = state.referenceCoordinate;
541 for (int i = 0; i < 5; ++i) {
542 linRef.parameters[i] = state.parameters[i];
543 }
544
545 const bool ok = Propagator::propagateToReference(state, linRef, state.referenceCoordinate, 0.5f);
546 BOOST_REQUIRE(ok);
547 BOOST_CHECK(covarianceSatisfiesDeclaredInvariant(state));
548 BOOST_CHECK_CLOSE(state.covariance[packedCovarianceIndex(1, 0)], 2.f, 1e-3f); // sqrt(4*1) = 2, sign-preserved.
549}
550
551BOOST_AUTO_TEST_CASE(ForwardUpdateSanitizesReproducer)
552{
554 state.kind = SurfaceKind::Disk;
555 state.referenceCoordinate = -67.6889038f;
556 state.alpha = 0.f;
557 state.parameters[0] = -3.40663648f;
558 state.parameters[1] = -3.04799104f;
559 state.parameters[2] = -2.40926218f;
560 state.parameters[3] = -15.2632132f;
561 state.parameters[4] = -0.0805783421f;
562 state.absCharge = 1;
563 state.pid = o2::track::PID::Pion;
564 const float cov[15] = {
565 5.45968469e-05f, 7.92145147e-05f, 2.34508334e-05f, 0.000361069426f, 9.60996113e-05f, 0.00121101411f,
566 0.00140110496f, 0.0018511567f, 0.00489055132f, 0.0514357649f,
567 0.117691882f, 0.0217776336f, 0.452787817f, 1.21933973f, 168.588654f};
568 for (int i = 0; i < 15; ++i) {
569 state.covariance[i] = cov[i];
570 }
571 SurfaceMeasurement meas{};
572 meas.frame.u = -3.4059999f;
573 meas.frame.v = -3.04678011f;
574 meas.covariance.uu = 4.4239976e-05f;
575 meas.covariance.vv = 0.000105412393f;
576 float chi2 = 0.f;
577
578 BOOST_REQUIRE(Propagator::updateForward(state, meas, chi2));
579 BOOST_CHECK(allDiagonalsNonNegative(state));
580}
581
582// --- 6. Operation failure remains transactional: a failing rotate/propagate
583// call must leave the input state byte-for-byte unchanged -- the
584// new sanitization call must never run (and never partially mutate state)
585// on a failure path.
586
587BOOST_AUTO_TEST_CASE(FailingBarrelRotateLeavesStateUnchanged)
588{
590 state.kind = SurfaceKind::Cylinder;
592 state.alpha = 0.3f;
593 state.parameters[0] = 1.25f;
594 state.parameters[1] = -0.75f;
595 state.parameters[2] = 1.5f; // |Snp| >= 1: rotate must reject before touching anything.
596 state.parameters[3] = -0.35f;
597 state.parameters[4] = 0.8f;
598 state.absCharge = 1;
599 state.pid = o2::track::PID::Pion;
600 for (uint8_t i = 0; i < 5; ++i) {
601 state.covariance[packedCovarianceIndex(i, i)] = 0.01f;
602 }
603 const SurfaceTrackState original = state;
604
605 const bool ok = Propagator::rotateBarrel(state, state.alpha + 3.0f); // Large rotation: local direction inversion.
606
607 BOOST_CHECK(!ok);
608 BOOST_CHECK(bitEqual(state, original));
609}
particle ids, masses, names class definition
int32_t i
SurfaceTrackState state
float chi2
uint32_t j
Definition RawData.h:0
GLboolean GLboolean GLboolean b
Definition glcorearb.h:1233
GLboolean GLboolean GLboolean GLboolean a
Definition glcorearb.h:1233
SurfaceTrackState makeOverRangeBarrelState()
BOOST_AUTO_TEST_CASE(SanitizeCovarianceAbsNegativeDiagonal)
SurfaceTrackState makeOverCorrelatedForwardState()
BOOST_CHECK(tree)
BOOST_CHECK_EQUAL(triggersD.size(), triggers.size())