Project
Loading...
Searching...
No Matches
testPropagator.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#define BOOST_TEST_MODULE ITSMFTPropagator
13#define BOOST_TEST_MAIN
14#define BOOST_TEST_DYN_LINK
15#include <boost/test/unit_test.hpp>
16
17#include <algorithm>
18#include <array>
19#include <cmath>
20#include <cstring>
21#include <limits>
22#include <sstream>
23
28
29#if __has_include("ITSMFTTracking/detail/SurfaceStateOperations.h") || __has_include("ITSMFTTracking/BarrelSurfaceStateOperations.h") || __has_include("ITSMFTTracking/ForwardSurfaceStateOperations.h")
30#error "coordinate-family state operations must be declared in Propagator.h"
31#endif
32
33using namespace o2::itsmft::tracking;
34
35namespace
36{
37
38template <typename T>
39bool bitEqual(const T& lhs, const T& rhs)
40{
41 return std::memcmp(&lhs, &rhs, sizeof(T)) == 0;
42}
43
44// --- Barrel fixtures (same convention as testRefitHit.cxx's barrelState()) --
45
46SurfaceTrackState barrelState(uint8_t absCharge = 1, o2::track::PID pid = o2::track::PID::Pion)
47{
49 state.parameters[0] = 1.25f;
50 state.parameters[1] = -0.75f;
51 state.parameters[2] = 0.2f;
52 state.parameters[3] = -0.35f;
53 state.parameters[4] = 0.8f;
55 state.alpha = 0.3f;
56 state.kind = SurfaceKind::Cylinder;
57 state.absCharge = absCharge;
58 state.pid = pid;
59 for (uint8_t row = 0; row < 5; ++row) {
60 for (uint8_t column = 0; column <= row; ++column) {
61 state.covariance[packedCovarianceIndex(row, column)] = row == column ? 0.01f * (row + 1) : 0.0002f * (row + column + 1);
62 }
63 }
64 return state;
65}
66
68{
70}
71
72SurfaceMeasurement barrelMeasurement()
73{
74 SurfaceMeasurement measurement{};
75 measurement.frame.q = 2.5f;
76 measurement.frame.frameAngle = 0.3f; // same alpha as barrelState(): no rotation needed
77 measurement.frame.u = 0.8f;
78 measurement.frame.v = -0.45f;
79 measurement.covariance = {0.04f, 0.012f, 0.09f};
80 return measurement;
81}
82
83constexpr float BarrelBz = 5.f;
84
85SurfaceDescriptor cylinderDescriptor(NominalSurfaceMaterial material)
86{
87 SurfaceDescriptor descriptor{};
88 descriptor.kind = SurfaceKind::Cylinder;
89 descriptor.referenceCoordinate = 2.5f;
90 descriptor.material = material;
91 return descriptor;
92}
93
94// --- Disk fixtures (same convention as testRefitHit.cxx's diskState()) -----
95
96SurfaceTrackState diskState(uint8_t absCharge = 1, o2::track::PID pid = o2::track::PID::Pion)
97{
99 state.parameters[0] = 1.25f;
100 state.parameters[1] = -0.75f;
101 state.parameters[2] = 0.35f;
102 state.parameters[3] = -2.5f;
103 state.parameters[4] = 0.8f;
105 state.kind = SurfaceKind::Disk;
106 state.absCharge = absCharge;
107 state.pid = pid;
108 for (uint8_t row = 0; row < 5; ++row) {
109 for (uint8_t column = 0; column <= row; ++column) {
110 state.covariance[packedCovarianceIndex(row, column)] = row == column ? 0.01f * (row + 1) : 0.0002f * (row + column + 1);
111 }
112 }
113 return state;
114}
115
117{
119}
120
121SurfaceMeasurement diskMeasurement()
122{
123 SurfaceMeasurement measurement{};
124 measurement.frame = {-50.f, 0.8f, -0.45f, 0.f};
125 measurement.frame.q = -50.f;
126 measurement.frame.u = 0.8f;
127 measurement.frame.v = -0.45f;
128 measurement.covariance = {0.04f, 0.f, 0.09f};
129 return measurement;
130}
131
132constexpr float DiskBz = 5.f;
133
134SurfaceDescriptor diskDescriptor(NominalSurfaceMaterial material)
135{
136 SurfaceDescriptor descriptor{};
137 descriptor.kind = SurfaceKind::Disk;
138 descriptor.referenceCoordinate = -50.f;
139 descriptor.material = material;
140 return descriptor;
141}
142
143// A stationary, zero-residual measurement leaves momentum unchanged by the
144// transport/update, exposing material effects through the public API.
145bool propagateThroughMaterial(SurfaceTrackState& state, SurfaceTrackParameters& reference,
148{
149 SurfaceDescriptor surface{};
150 surface.kind = state.kind;
151 surface.referenceCoordinate = state.referenceCoordinate;
152 surface.material = {budget.xOverX0, budget.arealDensityGPerCm2};
153 SurfaceMeasurement measurement{};
155 measurement.covariance = {0.04f, 0.f, 0.09f};
156 float chi2 = 0.f;
157 return Propagator::propagateToMeasurement(state, reference, surface, measurement, 0.f,
158 direction, false, 0.f, chi2, false);
159}
160
161bool propagateThroughMaterial(SurfaceTrackState& state, material::IntegratedMaterialBudget budget,
163{
165 return propagateThroughMaterial(state, reference, budget, direction);
166}
167
168// Independent double-precision helix intersections for numerical derivatives.
169// The target reference plane is fixed for every perturbed source state.
170std::array<double, 5> intersectConversionPlane(const SurfaceTrackState& source,
171 const std::array<double, 5>& p,
172 const SurfaceTrackState& target, double bz)
173{
174 double x = p[0], y = p[1], z = source.referenceCoordinate, phi = p[2];
175 if (source.kind == SurfaceKind::Cylinder) {
176 x = source.referenceCoordinate * std::cos(double(source.alpha)) - p[0] * std::sin(double(source.alpha));
177 y = source.referenceCoordinate * std::sin(double(source.alpha)) + p[0] * std::cos(double(source.alpha));
178 z = p[1];
179 phi = source.alpha + std::asin(p[2]);
180 }
181 const double curvature = p[4] * bz * o2::constants::math::B2C;
182 auto pointAt = [&](double path) {
183 const double halfAngle = curvature * path / 2.;
184 const double sinc = halfAngle == 0. ? 1. : std::sin(halfAngle) / halfAngle;
185 return std::array<double, 3>{x + path * sinc * std::cos(phi + halfAngle),
186 y + path * sinc * std::sin(phi + halfAngle), z + path * p[3]};
187 };
188 double path = 0.;
189 if (target.kind == SurfaceKind::Disk) {
190 path = (target.referenceCoordinate - z) / p[3];
191 const auto position = pointAt(path);
192 return {position[0], position[1], phi + curvature * path, p[3], p[4]};
193 }
194 const double csA = std::cos(double(target.alpha)), snA = std::sin(double(target.alpha));
195 // Newton iteration finds the local intersection continuously connected to
196 // the nominal point; it does not reuse the production Jacobian.
197 for (int iteration = 0; iteration < 6; ++iteration) {
198 const auto position = pointAt(path);
199 path -= (position[0] * csA + position[1] * snA - target.referenceCoordinate) /
200 std::cos(phi + curvature * path - target.alpha);
201 }
202 const auto position = pointAt(path);
203 return {-position[0] * snA + position[1] * csA, position[2],
204 std::sin(phi + curvature * path - target.alpha), p[3], p[4]};
205}
206
207void checkConversionCovariance(const SurfaceTrackState& source, float bz)
208{
209 auto target = source;
210
211 const auto targetKind = source.kind == SurfaceKind::Cylinder ? SurfaceKind::Disk : SurfaceKind::Cylinder;
212 BOOST_REQUIRE(Propagator::convertKind(target, targetKind, bz));
213 double jacobian[5][5]{};
214 std::array<double, 5> nominal{};
215 std::copy(std::begin(source.parameters), std::end(source.parameters), nominal.begin());
216 constexpr double step = 1.e-5;
217 for (int column = 0; column < 5; ++column) {
218 auto plus = nominal, minus = nominal;
219 plus[column] += step;
220 minus[column] -= step;
221 const auto high = intersectConversionPlane(source, plus, target, bz);
222 const auto low = intersectConversionPlane(source, minus, target, bz);
223 for (int row = 0; row < 5; ++row) {
224 jacobian[row][column] = (high[row] - low[row]) / (2. * step);
225 }
226 }
227 for (int row = 0; row < 5; ++row) {
228 for (int column = 0; column <= row; ++column) {
229 double expected = 0.;
230 for (int i = 0; i < 5; ++i) {
231 for (int j = 0; j < 5; ++j) {
232 expected += jacobian[row][i] * source.covariance[packedCovarianceIndex(i, j)] * jacobian[column][j];
233 }
234 }
235 const float actual = target.covariance[packedCovarianceIndex(row, column)];
236 BOOST_CHECK_SMALL(double(actual) - expected, 1.e-7 + 2.e-5 * std::abs(expected));
237 }
238 }
239}
240
241} // namespace
242
243BOOST_AUTO_TEST_CASE(ForwardHelixSmallAngleMomentumDerivative)
244{
245 // Isolate the q/pT Jacobian column with a unit momentum variance. An
246 // independent double-precision trajectory supplies numerical derivatives;
247 // checking only total position variances can hide this column's cancellation.
248 for (const float bz : {-5.f, 5.f}) {
249 for (const float qOverPt : {-20.f, -1.f, -0.1f, -0.01f, -1.e-8f, 1.e-8f, 0.01f, 0.1f, 1.f, 20.f}) {
250 for (const float dz : {-32.f, -1.4222f, -0.01f, 0.01f, 1.4222f, 32.f}) {
251 for (const float tanl : {-10.f, 10.f}) {
252 BOOST_TEST_CONTEXT("bz=" << bz << " q/pT=" << qOverPt << " dz=" << dz << " tanl=" << tanl)
253 {
254 auto source = diskState();
255 source.parameters[0] = source.parameters[1] = 0.f;
256 source.parameters[2] = 0.7f;
257 source.parameters[3] = tanl;
258 source.parameters[4] = qOverPt;
259 std::fill(std::begin(source.covariance), std::end(source.covariance), 0.f);
260 source.covariance[packedCovarianceIndex(4, 4)] = 1.f;
261 auto plane = source;
262 plane.referenceCoordinate += dz;
263 std::array<double, 5> parameters{};
264 std::copy(std::begin(source.parameters), std::end(source.parameters), parameters.begin());
265 const auto expected = intersectConversionPlane(source, parameters, plane, bz);
266 constexpr double step = 1.e-3;
267 auto plus = parameters, minus = parameters;
268 plus[4] += step;
269 minus[4] -= step;
270 const auto high = intersectConversionPlane(source, plus, plane, bz);
271 const auto low = intersectConversionPlane(source, minus, plane, bz);
272 std::array<double, 5> derivative{};
273 for (int row = 0; row < 5; ++row) {
274 derivative[row] = (high[row] - low[row]) / (2. * step);
275 }
276
277 auto direct = source;
278 auto referenced = source;
280 BOOST_REQUIRE(Propagator::propagateForward(direct, plane.referenceCoordinate, bz));
281 BOOST_REQUIRE(Propagator::propagateForward(referenced, reference, plane.referenceCoordinate, bz));
282 for (int row = 0; row < 5; ++row) {
283 const double positionTolerance = 2.e-7 * std::abs(expected[row]) + 1.e-12;
284 BOOST_CHECK_SMALL(double(direct.parameters[row]) - expected[row], positionTolerance);
285 BOOST_CHECK_SMALL(double(referenced.parameters[row]) - expected[row], positionTolerance);
286 BOOST_CHECK_SMALL(double(reference.parameters[row]) - expected[row], positionTolerance);
287 for (int column = 0; column <= row; ++column) {
288 const auto index = packedCovarianceIndex(row, column);
289 const double covariance = derivative[row] * derivative[column];
290 const double tolerance = 2.e-5 * std::abs(covariance) + 1.e-16;
291 BOOST_CHECK_SMALL(double(direct.covariance[index]) - covariance, tolerance);
292 BOOST_CHECK_SMALL(double(referenced.covariance[index]) - covariance, tolerance);
293 }
294 }
295 }
296 }
297 }
298 }
299 }
300}
301
302BOOST_AUTO_TEST_CASE(ForwardHelixFloatSeriesBoundary)
303{
304 // Isolate the q/pT Jacobian column with a unit momentum variance. An
305 // independent double-precision trajectory supplies numerical derivatives;
306 // checking only total position variances can hide this column's cancellation.
307 for (const float bz : {-5.f, 5.f}) {
308 for (const float halfAngle : {-0.5f, -0.2501f, -0.25f, -0.2499f, 0.2499f, 0.25f, 0.2501f, 0.5f}) {
309 for (const float dz : {-32.f, 32.f}) {
310 for (const float tanl : {-2.5f, 2.5f}) {
311 const float qOverPt = halfAngle / (0.5f * o2::constants::math::B2C * bz * dz / tanl);
312 BOOST_TEST_CONTEXT("bz=" << bz << " q/pT=" << qOverPt << " dz=" << dz << " tanl=" << tanl)
313 {
314 auto source = diskState();
315 source.parameters[0] = source.parameters[1] = 0.f;
316 source.parameters[2] = 0.7f;
317 source.parameters[3] = tanl;
318 source.parameters[4] = qOverPt;
319 std::fill(std::begin(source.covariance), std::end(source.covariance), 0.f);
320 source.covariance[packedCovarianceIndex(4, 4)] = 1.f;
321 auto plane = source;
322 plane.referenceCoordinate += dz;
323 std::array<double, 5> parameters{};
324 std::copy(std::begin(source.parameters), std::end(source.parameters), parameters.begin());
325 const auto expected = intersectConversionPlane(source, parameters, plane, bz);
326 constexpr double step = 1.e-3;
327 auto plus = parameters, minus = parameters;
328 plus[4] += step;
329 minus[4] -= step;
330 const auto high = intersectConversionPlane(source, plus, plane, bz);
331 const auto low = intersectConversionPlane(source, minus, plane, bz);
332 std::array<double, 5> derivative{};
333 for (int row = 0; row < 5; ++row) {
334 derivative[row] = (high[row] - low[row]) / (2. * step);
335 }
336
337 auto direct = source;
338 auto referenced = source;
340 BOOST_REQUIRE(Propagator::propagateForward(direct, plane.referenceCoordinate, bz));
341 BOOST_REQUIRE(Propagator::propagateForward(referenced, reference, plane.referenceCoordinate, bz));
342 for (int row = 0; row < 5; ++row) {
343 // Bound rounding by the operands rather than a possibly cancelling
344 // final angle/component. The covariance/Jacobian tolerance below
345 // remains unchanged from the small-angle regression.
346 const double path = double(dz) / tanl;
347 const double scale = row < 2 ? std::abs(path)
348 : row == 2 ? std::abs(parameters[2]) + 2. * std::abs(double(halfAngle))
349 : std::abs(expected[row]);
350 const double positionTolerance = 4. * std::numeric_limits<float>::epsilon() * scale + 1.e-12;
351 BOOST_CHECK_SMALL(double(direct.parameters[row]) - expected[row], positionTolerance);
352 BOOST_CHECK_SMALL(double(referenced.parameters[row]) - expected[row], positionTolerance);
353 BOOST_CHECK_SMALL(double(reference.parameters[row]) - expected[row], positionTolerance);
354 for (int column = 0; column <= row; ++column) {
355 const auto index = packedCovarianceIndex(row, column);
356 const double covariance = derivative[row] * derivative[column];
357 const double tolerance = 2.e-5 * std::abs(covariance) + 1.e-16;
358 BOOST_CHECK_SMALL(double(direct.covariance[index]) - covariance, tolerance);
359 BOOST_CHECK_SMALL(double(referenced.covariance[index]) - covariance, tolerance);
360 }
361 }
362 }
363 }
364 }
365 }
366 }
367}
368
369BOOST_AUTO_TEST_CASE(ForwardHelixTransportMatchesNumericalDerivatives)
370{
371 for (const float bz : {-5.f, 5.f}) {
372 for (const float qOverPt : {-20.f, -0.1f, 0.1f, 20.f}) {
373 for (const float dz : {-32.f, 32.f}) {
374 BOOST_TEST_CONTEXT("bz=" << bz << " q/pT=" << qOverPt << " dz=" << dz)
375 {
376 auto source = diskState();
377 source.parameters[4] = qOverPt;
378 auto plane = source;
379 plane.referenceCoordinate += dz;
380 std::array<double, 5> parameters{};
381 std::copy(std::begin(source.parameters), std::end(source.parameters), parameters.begin());
382 const auto expected = intersectConversionPlane(source, parameters, plane, bz);
383 double jacobian[5][5]{};
384 constexpr double step = 1.e-5;
385 for (int column = 0; column < 5; ++column) {
386 auto plus = parameters, minus = parameters;
387 plus[column] += step;
388 minus[column] -= step;
389 const auto high = intersectConversionPlane(source, plus, plane, bz);
390 const auto low = intersectConversionPlane(source, minus, plane, bz);
391 for (int row = 0; row < 5; ++row) {
392 jacobian[row][column] = (high[row] - low[row]) / (2. * step);
393 }
394 }
395 auto direct = source;
396 auto referenced = source;
398 std::array<double, 5> difference{};
399 for (int row = 0; row < 5; ++row) {
400 referenced.parameters[row] += 0.001f * (row + 1);
401 difference[row] = double(referenced.parameters[row]) - source.parameters[row];
402 }
403 BOOST_REQUIRE(Propagator::propagateForward(direct, plane.referenceCoordinate, bz));
404 BOOST_REQUIRE(Propagator::propagateForward(referenced, reference, plane.referenceCoordinate, bz));
405 for (int row = 0; row < 5; ++row) {
406 double linearized = expected[row];
407 for (int i = 0; i < 5; ++i) {
408 linearized += jacobian[row][i] * difference[i];
409 }
410 BOOST_CHECK_SMALL(double(direct.parameters[row]) - expected[row], 2.e-6);
411 BOOST_CHECK_SMALL(double(referenced.parameters[row]) - linearized, 3.e-6);
412 for (int column = 0; column <= row; ++column) {
413 double covariance = 0.;
414 for (int i = 0; i < 5; ++i) {
415 for (int j = 0; j < 5; ++j) {
416 covariance += jacobian[row][i] * source.covariance[packedCovarianceIndex(i, j)] * jacobian[column][j];
417 }
418 }
419 const auto index = packedCovarianceIndex(row, column);
420 const double tolerance = 1.e-7 + 2.e-5 * std::abs(covariance);
421 BOOST_CHECK_SMALL(double(direct.covariance[index]) - covariance, tolerance);
422 BOOST_CHECK_SMALL(double(referenced.covariance[index]) - covariance, tolerance);
423 }
424 }
425 }
426 }
427 }
428 }
429}
430
431// --- 1/2: same-family propagate-to-measurement succeeds ---------------------
432
433BOOST_AUTO_TEST_CASE(CylinderToCylinderPropagateAndUpdateSucceeds)
434{
435 auto state = barrelState();
436 auto linRef = barrelLinRef(state);
437 const auto measurement = barrelMeasurement();
438 const auto descriptor = cylinderDescriptor(NominalSurfaceMaterial{0.f, 0.f});
439 float chi2 = 0.f;
440
441 BOOST_REQUIRE(Propagator::propagateToMeasurement(state, linRef, descriptor, measurement, BarrelBz,
442 material::MaterialTraversalDirection::AlongMomentum,
443 false, 0.f, chi2, false));
444 BOOST_CHECK_EQUAL(static_cast<int>(state.kind), static_cast<int>(SurfaceKind::Cylinder));
445 BOOST_CHECK_EQUAL(state.referenceCoordinate, measurement.frame.q);
446 BOOST_CHECK(std::isfinite(chi2));
447 BOOST_CHECK_GE(chi2, 0.f);
448}
449
450BOOST_AUTO_TEST_CASE(DiskToDiskPropagateAndUpdateSucceeds)
451{
452 auto state = diskState();
453 auto linRef = diskLinRef(state);
454 const auto measurement = diskMeasurement();
455 const auto descriptor = diskDescriptor(NominalSurfaceMaterial{0.f, 0.f});
456 float chi2 = 0.f;
457
458 BOOST_REQUIRE(Propagator::propagateToMeasurement(state, linRef, descriptor, measurement, DiskBz,
459 material::MaterialTraversalDirection::AlongMomentum,
460 false, 0.f, chi2, false));
461 BOOST_CHECK_EQUAL(static_cast<int>(state.kind), static_cast<int>(SurfaceKind::Disk));
462 BOOST_CHECK_EQUAL(state.referenceCoordinate, measurement.frame.q);
463 BOOST_CHECK(std::isfinite(chi2));
464 BOOST_CHECK_GE(chi2, 0.f);
465}
466
467BOOST_AUTO_TEST_CASE(AcceptedForwardPropagationSelectsFieldAndLowFieldPaths)
468{
469 auto fieldOn = diskState();
470 auto lowPositive = diskState();
471 auto lowNegative = diskState();
472
473 BOOST_REQUIRE(Propagator::propagateToReference(fieldOn, -50.f, 5.f));
474 BOOST_REQUIRE(Propagator::propagateToReference(lowPositive, -50.f, 0.01f));
475 BOOST_REQUIRE(Propagator::propagateToReference(lowNegative, -50.f, -0.01f));
476 BOOST_CHECK(bitEqual(lowPositive, lowNegative));
477 BOOST_CHECK(!bitEqual(fieldOn, lowPositive));
478}
479
480// --- 3: compatible-family propagation and material effects -----------------
481
482BOOST_AUTO_TEST_CASE(CompatibleFamilyMatchesDirectBarrelPrimitiveReplayWithoutMaterial)
483{
484 auto viaPropagator = barrelState();
485 auto viaPropagatorRef = barrelLinRef(viaPropagator);
486 auto viaDirect = viaPropagator;
487 auto viaDirectRef = viaPropagatorRef;
488 const auto measurement = barrelMeasurement();
489 const auto material = NominalSurfaceMaterial{0.f, 0.f};
490 const auto descriptor = cylinderDescriptor(material);
491 float chi2Propagator = 0.f;
492 float chi2Direct = 0.f;
493
494 BOOST_REQUIRE(Propagator::propagateToMeasurement(viaPropagator, viaPropagatorRef, descriptor, measurement, BarrelBz,
495 material::MaterialTraversalDirection::OppositeMomentum,
496 false, 0.f, chi2Propagator, true));
497
498 BOOST_REQUIRE(Propagator::rotateBarrel(viaDirect, viaDirectRef, measurement.frame.frameAngle, BarrelBz));
499 BOOST_REQUIRE(Propagator::propagateBarrel(viaDirect, viaDirectRef, measurement.frame.q, BarrelBz));
500 float predChi2 = 0.f;
501 BOOST_REQUIRE(Propagator::predictedChi2Barrel(viaDirect, measurement, predChi2));
502 float updateChi2 = 0.f;
503 BOOST_REQUIRE(Propagator::updateBarrel(viaDirect, measurement, updateChi2));
504 chi2Direct = updateChi2;
505 BOOST_REQUIRE(Propagator::shiftReferenceToMeasurementBarrel(viaDirectRef, measurement));
506
507 BOOST_CHECK(bitEqual(viaPropagator, viaDirect));
508 BOOST_CHECK(bitEqual(viaPropagatorRef, viaDirectRef));
509 BOOST_CHECK_EQUAL(chi2Propagator, chi2Direct);
510}
511
512BOOST_AUTO_TEST_CASE(BarrelMaterialUsesLegacyIncidencePathLength)
513{
514 auto state = barrelState();
515 state.parameters[2] = 0.6f;
516 state.parameters[3] = 1.2f;
517 const auto original = state;
518 const material::IntegratedMaterialBudget nominalMaterial{0.01f, 0.001f};
519
520 const float snp = original.parameters[2];
521 const float tgl = original.parameters[3];
522 const float incidenceScale = std::sqrt((1.f + tgl * tgl) / ((1.f - snp) * (1.f + snp)));
523 const material::IntegratedMaterialBudget legacyMaterial{
524 nominalMaterial.xOverX0 * incidenceScale,
525 nominalMaterial.arealDensityGPerCm2 * incidenceScale};
526 const float transverseMomentum = static_cast<float>(original.absCharge) / std::abs(original.parameters[4]);
527 const float momentum = transverseMomentum * std::sqrt(1.f + tgl * tgl);
528
529 float expectedMomentum = 0.f;
530 float expectedTheta2 = 0.f;
531 float expectedVariance = 0.f;
532 const bool expected = material::calculateMaterialPhysics(momentum, original.pid, original.absCharge,
533 material::MaterialTraversalDirection::AlongMomentum,
534 legacyMaterial, expectedMomentum, expectedTheta2, expectedVariance);
535
536 float uncorrectedMomentum = 0.f;
537 float uncorrectedTheta2 = 0.f;
538 float uncorrectedVariance = 0.f;
539 const bool uncorrected = material::calculateMaterialPhysics(momentum, original.pid, original.absCharge,
540 material::MaterialTraversalDirection::AlongMomentum,
541 nominalMaterial, uncorrectedMomentum, uncorrectedTheta2, uncorrectedVariance);
542 const auto result = propagateThroughMaterial(state, nominalMaterial,
543 material::MaterialTraversalDirection::AlongMomentum);
544
545 BOOST_REQUIRE(expected);
546 BOOST_REQUIRE(uncorrected);
547 BOOST_REQUIRE(result);
548
549 BOOST_CHECK_EQUAL(state.parameters[4], (original.parameters[4] * momentum) / expectedMomentum);
550 BOOST_CHECK_GT(expectedTheta2, uncorrectedTheta2);
551 BOOST_CHECK_LT(expectedMomentum, uncorrectedMomentum);
552}
553
554BOOST_AUTO_TEST_CASE(LinearizedBarrelMaterialUsesLegacyReferenceIncidence)
555{
556 auto state = barrelState();
557 state.parameters[2] = 0.1f;
558 state.parameters[3] = 0.2f;
559 auto linRef = barrelLinRef(state);
560 linRef.parameters[2] = 0.6f;
561 linRef.parameters[3] = 1.2f;
562 const float stateQ2PtBefore = state.parameters[4];
563 const float referenceQ2PtBefore = linRef.parameters[4];
564 const material::IntegratedMaterialBudget nominalMaterial{0.01f, 0.001f};
565
566 const float snp = linRef.parameters[2];
567 const float tgl = linRef.parameters[3];
568 const float incidenceScale = std::sqrt((1.f + tgl * tgl) / ((1.f - snp) * (1.f + snp)));
569 const material::IntegratedMaterialBudget legacyMaterial{
570 nominalMaterial.xOverX0 * incidenceScale,
571 nominalMaterial.arealDensityGPerCm2 * incidenceScale};
572 const float stateTgl = state.parameters[3];
573 const float transverseMomentum = static_cast<float>(state.absCharge) / std::abs(state.parameters[4]);
574 const float momentum = transverseMomentum * std::sqrt(1.f + stateTgl * stateTgl);
575
576 float expectedMomentum = 0.f;
577 float expectedTheta2 = 0.f;
578 float expectedVariance = 0.f;
580 material::MaterialTraversalDirection::AlongMomentum,
581 legacyMaterial, expectedMomentum, expectedTheta2, expectedVariance);
582 const auto result = propagateThroughMaterial(state, linRef, nominalMaterial,
583 material::MaterialTraversalDirection::AlongMomentum);
584
585 BOOST_REQUIRE(expected);
586 BOOST_REQUIRE(result);
587
588 const float expectedStateQ2Pt = (stateQ2PtBefore * momentum) / expectedMomentum;
589 const float expectedReferenceQ2Pt = (referenceQ2PtBefore * momentum) / expectedMomentum;
590 BOOST_CHECK_EQUAL(state.parameters[4], expectedStateQ2Pt);
591 BOOST_CHECK_EQUAL(linRef.parameters[4], expectedReferenceQ2Pt);
592}
593
594BOOST_AUTO_TEST_CASE(LinearizedBarrelMaterialKeepsReferenceQ2PtForMCSOnly)
595{
596 auto state = barrelState();
597 auto linRef = barrelLinRef(state);
598 const auto referenceBefore = linRef;
599
600 const auto result = propagateThroughMaterial(
601 state, linRef, material::IntegratedMaterialBudget{0.01f, 0.f},
602 material::MaterialTraversalDirection::AlongMomentum);
603
604 BOOST_REQUIRE(result);
605
606 BOOST_CHECK(bitEqual(linRef, referenceBefore));
607}
608
609BOOST_AUTO_TEST_CASE(FailingLinearizedBarrelMaterialLeavesStateAndReferenceUnchanged)
610{
611 auto state = barrelState();
612 auto linRef = barrelLinRef(state);
613 const auto stateBefore = state;
614 const auto referenceBefore = linRef;
615
616 const auto result = propagateThroughMaterial(
617 state, linRef, material::IntegratedMaterialBudget{1.e8f, 0.f},
618 material::MaterialTraversalDirection::AlongMomentum);
619
621
622 BOOST_CHECK(bitEqual(state, stateBefore));
623 BOOST_CHECK(bitEqual(linRef, referenceBefore));
624}
625
626BOOST_AUTO_TEST_CASE(CompatibleFamilyMatchesDirectForwardPrimitiveReplayWithoutMaterial)
627{
628 auto viaPropagator = diskState();
629 auto viaPropagatorRef = diskLinRef(viaPropagator);
630 auto viaDirect = viaPropagator;
631 auto viaDirectRef = viaPropagatorRef;
632 const auto measurement = diskMeasurement();
633 const auto material = NominalSurfaceMaterial{0.f, 0.f};
634 const auto descriptor = diskDescriptor(material);
635 float chi2Propagator = 0.f;
636 float chi2Direct = 0.f;
637
638 BOOST_REQUIRE(Propagator::propagateToMeasurement(viaPropagator, viaPropagatorRef, descriptor, measurement, DiskBz,
639 material::MaterialTraversalDirection::OppositeMomentum,
640 false, 0.f, chi2Propagator, true));
641
642 BOOST_REQUIRE(Propagator::propagateForward(viaDirect, viaDirectRef, measurement.frame.q, DiskBz));
643 float predChi2 = 0.f;
644 BOOST_REQUIRE(Propagator::predictedChi2Forward(viaDirect, measurement, predChi2));
645 float updateChi2 = 0.f;
646 BOOST_REQUIRE(Propagator::updateForward(viaDirect, measurement, updateChi2));
647 chi2Direct = updateChi2;
648 BOOST_REQUIRE(Propagator::shiftReferenceToMeasurementForward(viaDirectRef, measurement));
649
650 BOOST_CHECK(bitEqual(viaPropagator, viaDirect));
651 BOOST_CHECK(bitEqual(viaPropagatorRef, viaDirectRef));
652 BOOST_CHECK_EQUAL(chi2Propagator, chi2Direct);
653}
654
655BOOST_AUTO_TEST_CASE(ForwardMaterialUsesLegacyIncidencePathLength)
656{
657 auto state = diskState();
658 state.parameters[3] = -0.5f;
659 const auto original = state;
660 const material::IntegratedMaterialBudget nominalMaterial{0.01f, 0.001f};
661
662 const float tgl = original.parameters[3];
663 const float incidenceScale = std::sqrt(1.f + tgl * tgl) / std::abs(tgl);
664 const material::IntegratedMaterialBudget legacyMaterial{
665 nominalMaterial.xOverX0 * incidenceScale,
666 nominalMaterial.arealDensityGPerCm2 * incidenceScale};
667 const float transverseMomentum = static_cast<float>(original.absCharge) / std::abs(original.parameters[4]);
668 const float momentum = transverseMomentum * std::sqrt(1.f + tgl * tgl);
669
670 float expectedMomentum = 0.f;
671 float expectedTheta2 = 0.f;
672 float expectedVariance = 0.f;
673 const bool expected = material::calculateMaterialPhysics(momentum, original.pid, original.absCharge,
674 material::MaterialTraversalDirection::AlongMomentum,
675 legacyMaterial, expectedMomentum, expectedTheta2, expectedVariance);
676
677 float uncorrectedMomentum = 0.f;
678 float uncorrectedTheta2 = 0.f;
679 float uncorrectedVariance = 0.f;
680 const bool uncorrected = material::calculateMaterialPhysics(momentum, original.pid, original.absCharge,
681 material::MaterialTraversalDirection::AlongMomentum,
682 nominalMaterial, uncorrectedMomentum, uncorrectedTheta2, uncorrectedVariance);
683 const auto result = propagateThroughMaterial(state, nominalMaterial,
684 material::MaterialTraversalDirection::AlongMomentum);
685
686 BOOST_REQUIRE(expected);
687 BOOST_REQUIRE(uncorrected);
688 BOOST_REQUIRE(result);
689
690 BOOST_CHECK_EQUAL(state.parameters[4], (original.parameters[4] * momentum) / expectedMomentum);
691 BOOST_CHECK_GT(expectedTheta2, uncorrectedTheta2);
692 BOOST_CHECK_LT(expectedMomentum, uncorrectedMomentum);
693}
694
695BOOST_AUTO_TEST_CASE(LinearizedForwardMaterialUsesReferenceIncidence)
696{
697 auto state = diskState();
698 auto linRef = diskLinRef(state);
699 linRef.parameters[3] = -0.5f;
700 const float stateQ2PtBefore = state.parameters[4];
701 const float referenceQ2PtBefore = linRef.parameters[4];
702 const material::IntegratedMaterialBudget nominalMaterial{0.01f, 0.001f};
703
704 const float referenceTgl = linRef.parameters[3];
705 const float incidenceScale = std::sqrt(1.f + referenceTgl * referenceTgl) / std::abs(referenceTgl);
706 const material::IntegratedMaterialBudget scaledMaterial{
707 nominalMaterial.xOverX0 * incidenceScale,
708 nominalMaterial.arealDensityGPerCm2 * incidenceScale};
709 const float stateTgl = state.parameters[3];
710 const float transverseMomentum = static_cast<float>(state.absCharge) / std::abs(state.parameters[4]);
711 const float momentum = transverseMomentum * std::sqrt(1.f + stateTgl * stateTgl);
712
713 float expectedMomentum = 0.f;
714 float expectedTheta2 = 0.f;
715 float expectedVariance = 0.f;
717 material::MaterialTraversalDirection::AlongMomentum,
718 scaledMaterial, expectedMomentum, expectedTheta2, expectedVariance);
719 const auto result = propagateThroughMaterial(state, linRef, nominalMaterial,
720 material::MaterialTraversalDirection::AlongMomentum);
721
722 BOOST_REQUIRE(expected);
723 BOOST_REQUIRE(result);
724
725 const float expectedStateQ2Pt = (stateQ2PtBefore * momentum) / expectedMomentum;
726 const float expectedReferenceQ2Pt = (referenceQ2PtBefore * momentum) / expectedMomentum;
727 BOOST_CHECK_EQUAL(state.parameters[4], expectedStateQ2Pt);
728 BOOST_CHECK_EQUAL(linRef.parameters[4], expectedReferenceQ2Pt);
729}
730
731BOOST_AUTO_TEST_CASE(LinearizedForwardMaterialKeepsReferenceQ2PtForMCSOnly)
732{
733 auto state = diskState();
734 auto linRef = diskLinRef(state);
735 const auto referenceBefore = linRef;
736
737 const auto result = propagateThroughMaterial(
738 state, linRef, material::IntegratedMaterialBudget{0.01f, 0.f},
739 material::MaterialTraversalDirection::AlongMomentum);
740
741 BOOST_REQUIRE(result);
742
743 BOOST_CHECK(bitEqual(linRef, referenceBefore));
744}
745
746BOOST_AUTO_TEST_CASE(FailingLinearizedForwardMaterialLeavesStateAndReferenceUnchanged)
747{
748 auto state = diskState();
749 auto linRef = diskLinRef(state);
750 const auto stateBefore = state;
751 const auto referenceBefore = linRef;
752
753 const auto result = propagateThroughMaterial(
754 state, linRef, material::IntegratedMaterialBudget{1.e8f, 0.f},
755 material::MaterialTraversalDirection::AlongMomentum);
756
758
759 BOOST_CHECK(bitEqual(state, stateBefore));
760 BOOST_CHECK(bitEqual(linRef, referenceBefore));
761}
762
763BOOST_AUTO_TEST_CASE(MaterialPropagationRejectsMismatchedReferenceKinds)
764{
765 for (const auto original : {barrelState(), diskState()}) {
766 auto state = original;
768 reference.kind = state.kind == SurfaceKind::Cylinder ? SurfaceKind::Disk : SurfaceKind::Cylinder;
769 const auto referenceBefore = reference;
770 BOOST_CHECK(!propagateThroughMaterial(state, reference, {0.01f, 0.001f},
771 material::MaterialTraversalDirection::AlongMomentum));
772 BOOST_CHECK(bitEqual(state, original));
773 BOOST_CHECK(bitEqual(reference, referenceBefore));
774 }
775}
776
777// --- 4: incompatible family converts, then propagates -----------------------
778
779BOOST_AUTO_TEST_CASE(BarrelStateConvertsToForwardThenPropagatesToDiskMeasurement)
780{
781 auto state = barrelState();
782 auto linRef = barrelLinRef(state);
783 const auto poisonState = state;
784
785 // A disk far enough along z that the converted (Forward) state can reach it.
786 SurfaceMeasurement measurement{};
787 measurement.frame.q = -10.f;
788 measurement.frame.u = 5.f;
789 measurement.frame.v = -5.f;
790 measurement.covariance = {10.f, 0.f, 10.f}; // loose: the point is not expected to land exactly here
791 const auto descriptor = diskDescriptor(NominalSurfaceMaterial{0.f, 0.f});
792 float chi2 = 0.f;
793
794 const bool ok = Propagator::propagateToMeasurement(state, linRef, descriptor, measurement, BarrelBz,
795 material::MaterialTraversalDirection::AlongMomentum,
796 false, 0.f, chi2, false);
797 BOOST_REQUIRE(ok);
798 BOOST_CHECK_EQUAL(static_cast<int>(state.kind), static_cast<int>(SurfaceKind::Disk));
799 BOOST_CHECK_EQUAL(state.referenceCoordinate, measurement.frame.q);
800 BOOST_CHECK_EQUAL(state.absCharge, poisonState.absCharge);
801 BOOST_CHECK(state.pid == poisonState.pid);
802 for (float value : state.parameters) {
803 BOOST_CHECK(std::isfinite(value));
804 }
805 for (float value : state.covariance) {
806 BOOST_CHECK(std::isfinite(value));
807 }
808}
809
810BOOST_AUTO_TEST_CASE(KindConversionRelinearizesAtConvertedState)
811{
812 auto nominalState = barrelState();
813 auto nominalRef = barrelLinRef(nominalState);
814 auto perturbedState = nominalState;
815 auto perturbedRef = nominalRef;
816 perturbedRef.parameters[0] += 0.1f;
817 perturbedRef.parameters[1] -= 0.2f;
818 perturbedRef.parameters[2] += 0.01f;
819 perturbedRef.parameters[3] -= 0.02f;
820 perturbedRef.parameters[4] += 0.001f;
821
822 SurfaceMeasurement measurement{};
823 measurement.frame.q = -10.f;
824 measurement.frame.u = 5.f;
825 measurement.frame.v = -5.f;
826 measurement.covariance = {10.f, 0.f, 10.f};
827 const auto descriptor = diskDescriptor(NominalSurfaceMaterial{0.f, 0.f});
828 float nominalChi2 = 0.f;
829 float perturbedChi2 = 0.f;
830
831 BOOST_REQUIRE(Propagator::propagateToMeasurement(nominalState, nominalRef, descriptor, measurement, BarrelBz,
832 material::MaterialTraversalDirection::AlongMomentum,
833 false, 0.f, nominalChi2, false));
834 BOOST_REQUIRE(Propagator::propagateToMeasurement(perturbedState, perturbedRef, descriptor, measurement, BarrelBz,
835 material::MaterialTraversalDirection::AlongMomentum,
836 false, 0.f, perturbedChi2, false));
837
838 BOOST_CHECK(bitEqual(perturbedState, nominalState));
839 BOOST_CHECK(bitEqual(perturbedRef, nominalRef));
840 BOOST_CHECK_EQUAL(perturbedChi2, nominalChi2);
841}
842
843BOOST_AUTO_TEST_CASE(ReverseKindConversionRelinearizesAtConvertedState)
844{
845 auto nominalState = diskState();
846 auto nominalRef = diskLinRef(nominalState);
847 auto perturbedState = nominalState;
848 auto perturbedRef = nominalRef;
849 perturbedRef.parameters[0] += 0.1f;
850 perturbedRef.parameters[1] -= 0.2f;
851 perturbedRef.parameters[2] += 0.01f;
852 perturbedRef.parameters[3] -= 0.02f;
853 perturbedRef.parameters[4] += 0.001f;
854
855 const auto measurement = barrelMeasurement();
856 const auto descriptor = cylinderDescriptor(NominalSurfaceMaterial{0.f, 0.f});
857 float nominalChi2 = 0.f;
858 float perturbedChi2 = 0.f;
859
860 BOOST_REQUIRE(Propagator::propagateToMeasurement(nominalState, nominalRef, descriptor, measurement, DiskBz,
861 material::MaterialTraversalDirection::AlongMomentum,
862 false, 0.f, nominalChi2, false));
863 BOOST_REQUIRE(Propagator::propagateToMeasurement(perturbedState, perturbedRef, descriptor, measurement, DiskBz,
864 material::MaterialTraversalDirection::AlongMomentum,
865 false, 0.f, perturbedChi2, false));
866
867 BOOST_CHECK(bitEqual(perturbedState, nominalState));
868 BOOST_CHECK(bitEqual(perturbedRef, nominalRef));
869 BOOST_CHECK_EQUAL(perturbedChi2, nominalChi2);
870}
871
872BOOST_AUTO_TEST_CASE(ConversionCovarianceMatchesFixedPlaneHelixDifferences)
873{
874 for (const float bz : {-5.f, 0.f, 5.f}) {
875 for (const float sign : {-1.f, 1.f}) {
876 auto barrel = barrelState();
877 barrel.parameters[3] *= sign;
878 barrel.parameters[4] *= sign;
879 checkConversionCovariance(barrel, bz);
880 auto disk = diskState();
881 disk.parameters[3] *= sign;
882 disk.parameters[4] *= sign;
883 checkConversionCovariance(disk, bz);
884 }
885 }
886}
887
888BOOST_AUTO_TEST_CASE(BarrelZUncertaintySurvivesConversionAndRoundTrip)
889{
890 auto state = barrelState();
891 state.alpha = 0.f;
893 state.parameters[0] = 0.f;
894 state.parameters[2] = 0.f;
895 state.parameters[3] = 2.f;
896 std::fill(std::begin(state.covariance), std::end(state.covariance), 0.f);
897 state.covariance[packedCovarianceIndex(1, 1)] = 1.f;
898 const auto before = state;
899
900 BOOST_REQUIRE(Propagator::convertKind(state, SurfaceKind::Disk, 5.f));
901 BOOST_CHECK_CLOSE(state.covariance[packedCovarianceIndex(0, 0)], 0.25f, 1.e-4f);
902 const float curvature = before.parameters[4] * 5.f * o2::constants::math::B2C;
903 BOOST_CHECK_CLOSE(state.covariance[packedCovarianceIndex(2, 0)], curvature / 4.f, 1.e-4f);
904 BOOST_REQUIRE(Propagator::convertKind(state, SurfaceKind::Cylinder, 5.f));
905 for (int i = 0; i < 15; ++i) {
906 BOOST_CHECK_SMALL(state.covariance[i] - before.covariance[i], 1.e-6f);
907 }
908}
909
910BOOST_AUTO_TEST_CASE(ConversionRejectsSingularAndNonFiniteInputsTransactionally)
911{
912 for (const float tanl : {0.f, std::numeric_limits<float>::quiet_NaN(), std::numeric_limits<float>::infinity()}) {
913 auto state = barrelState();
914 state.parameters[3] = tanl;
915 const auto before = state;
916
917 BOOST_CHECK(!Propagator::convertKind(state, SurfaceKind::Disk, 5.f));
918
919 BOOST_CHECK(bitEqual(state, before));
920 }
921}
922
923BOOST_AUTO_TEST_CASE(NonlinearAttachmentUsesTargetKindAndRollsBackAfterConversion)
924{
925 for (const bool startOnDisk : {false, true}) {
926 const auto source = startOnDisk ? diskState() : barrelState();
927 const auto target = startOnDisk ? cylinderDescriptor({0.f, 0.f}) : diskDescriptor({0.f, 0.f});
928 auto converted = source;
929
930 BOOST_REQUIRE(Propagator::convertKind(converted, target.kind, 0.f));
931 SurfaceMeasurement measurement{};
932 measurement.frame = {converted.referenceCoordinate, converted.parameters[0], converted.parameters[1], converted.alpha};
933 measurement.covariance = {0.04f, 0.f, 0.04f};
934 auto state = source;
935 float chi2 = 0.f;
936 BOOST_REQUIRE(Propagator::attachMeasurement(state, target, measurement, 0.f,
937 material::MaterialTraversalDirection::OppositeMomentum,
938 true, 100.f, chi2));
939 BOOST_CHECK(state.kind == target.kind);
940 for (int i = 0; i < 5; ++i) {
941 BOOST_CHECK_SMALL(state.parameters[i] - converted.parameters[i], 1.e-5f);
942 }
943 BOOST_CHECK_SMALL(chi2, 1.e-5f);
944
945 // Conversion may succeed while the measurement gate fails; neither the
946 // converted representation nor a partial chi2 may escape to the caller.
947 measurement.frame.u += 10.f;
948 state = source;
949 chi2 = 3.f;
950 BOOST_CHECK(!Propagator::attachMeasurement(state, target, measurement, 0.f,
951 material::MaterialTraversalDirection::OppositeMomentum,
952 true, 1.e-6f, chi2));
953
954 BOOST_CHECK(bitEqual(state, source));
956 }
957}
958
959BOOST_AUTO_TEST_CASE(ConvertFamilyPreservesChargeAndPID)
960{
961 auto state = barrelState(2, o2::track::PID::Kaon);
962
963 BOOST_REQUIRE(Propagator::convertKind(state, SurfaceKind::Disk, BarrelBz));
964 BOOST_CHECK_EQUAL(static_cast<int>(state.kind), static_cast<int>(SurfaceKind::Disk));
965 BOOST_CHECK_EQUAL(state.absCharge, uint8_t{2});
966 BOOST_CHECK(state.pid == o2::track::PID::Kaon);
967}
968
969BOOST_AUTO_TEST_CASE(ConvertFamilySameFamilyIsNoOpSuccess)
970{
971 auto state = barrelState();
972 const auto before = state;
973
974 BOOST_REQUIRE(Propagator::convertKind(state, SurfaceKind::Cylinder, DiskBz));
975 BOOST_CHECK(bitEqual(state, before));
976}
977
978// --- 5: degenerate conversion fails, transactionally ------------------------
979
980BOOST_AUTO_TEST_CASE(ForwardToBarrelConversionFailsAtOriginTransactionally)
981{
982 auto state = diskState();
983 state.parameters[0] = 0.f; // X
984 state.parameters[1] = 0.f; // Y: R == 0, alpha undefined
985 const auto poison = state;
986
987 BOOST_CHECK(!Propagator::convertKind(state, SurfaceKind::Cylinder, DiskBz));
988
989 BOOST_CHECK(bitEqual(state, poison));
990}
991
992BOOST_AUTO_TEST_CASE(ForwardToBarrelRejectsUnrepresentableDirectionsTransactionally)
993{
994 for (const float phi : {o2::constants::math::PI, -2.f, 2.f, o2::constants::math::PIHalf}) {
995 auto state = diskState();
996 state.parameters[0] = 10.f;
997 state.parameters[1] = 0.f;
998 state.parameters[2] = phi;
999 const auto before = state;
1000
1001 BOOST_CHECK(!Propagator::convertKind(state, SurfaceKind::Cylinder, DiskBz));
1002
1003 BOOST_CHECK(bitEqual(state, before));
1004 }
1005}
1006
1007// --- Zero-material and nonzero-material (MatLUT/nominal-material) paths -----
1008
1009BOOST_AUTO_TEST_CASE(ZeroMaterialPathSucceeds)
1010{
1011 auto state = barrelState();
1012 auto linRef = barrelLinRef(state);
1013 const auto measurement = barrelMeasurement();
1014 const auto descriptor = cylinderDescriptor(NominalSurfaceMaterial{0.f, 0.f});
1015 float chi2 = 0.f;
1016
1017 BOOST_REQUIRE(Propagator::propagateToMeasurement(state, linRef, descriptor, measurement, BarrelBz,
1018 material::MaterialTraversalDirection::AlongMomentum,
1019 false, 0.f, chi2, false));
1020}
1021
1022BOOST_AUTO_TEST_CASE(NonzeroNominalMaterialChangesResultRelativeToZeroMaterial)
1023{
1024 auto zeroState = barrelState();
1025 auto zeroRef = barrelLinRef(zeroState);
1026 auto materialState = barrelState();
1027 auto materialRef = barrelLinRef(materialState);
1028 const auto measurement = barrelMeasurement();
1029 const auto zeroDescriptor = cylinderDescriptor(NominalSurfaceMaterial{0.f, 0.f});
1030 const auto materialDescriptor = cylinderDescriptor(NominalSurfaceMaterial{0.05f, 0.01f});
1031 float zeroChi2 = 0.f;
1032 float materialChi2 = 0.f;
1033
1034 BOOST_REQUIRE(Propagator::propagateToMeasurement(zeroState, zeroRef, zeroDescriptor, measurement, BarrelBz,
1035 material::MaterialTraversalDirection::OppositeMomentum,
1036 false, 0.f, zeroChi2, false));
1037 BOOST_REQUIRE(Propagator::propagateToMeasurement(materialState, materialRef, materialDescriptor, measurement, BarrelBz,
1038 material::MaterialTraversalDirection::OppositeMomentum,
1039 false, 0.f, materialChi2, false));
1040
1041 // The material budget is read from the target SurfaceDescriptor (the
1042 // "MatLUT" mechanism, task requirement 6) -- not equal, not a parallel
1043 // model producing a byte-identical result either.
1044 BOOST_CHECK(!bitEqual(zeroState, materialState));
1045}
1046
1047// --- Holes are skipped by the native refit driver ----------------------------
1048
1049BOOST_AUTO_TEST_CASE(RefitDriverSkipsHoleSlots)
1050{
1051 auto state = barrelState();
1052 auto linRef = barrelLinRef(state);
1053 const auto measurement = barrelMeasurement();
1054
1055 std::array<SurfaceDescriptor, 1> surfaces{cylinderDescriptor(NominalSurfaceMaterial{0.f, 0.f})};
1056 SurfaceCatalogView catalog{surfaces.data(), static_cast<uint32_t>(surfaces.size())};
1057
1058 const detail::RefitMeasurementSlot present{measurement, LayerId{0}, true};
1059 const detail::RefitMeasurementSlot hole{};
1060
1061 std::array<detail::RefitMeasurementSlot, 3> slots{hole, present, hole};
1062 float chi2 = 0.f;
1063 uint32_t acceptedHitCount = 999;
1064
1065 BOOST_REQUIRE(detail::driveRefitLeg(state, linRef, chi2, acceptedHitCount, slots, catalog, BarrelBz,
1066 material::MaterialTraversalDirection::AlongMomentum, false, 100.f));
1067 BOOST_CHECK_EQUAL(acceptedHitCount, 1u);
1068}
1069
1070BOOST_AUTO_TEST_CASE(FullMFTRefitLegUsesNominalMaterialAtEverySurface)
1071{
1072 const SurfaceCatalogView catalog{kMFTSurfaces.data(), MFTNLayers};
1073 for (const auto direction : {material::MaterialTraversalDirection::AlongMomentum,
1074 material::MaterialTraversalDirection::OppositeMomentum}) {
1075 const bool alongMomentum = direction == material::MaterialTraversalDirection::AlongMomentum;
1076 auto state = diskState();
1077 state.referenceCoordinate = kMFTSurfaces[alongMomentum ? 0 : MFTNLayers - 1].referenceCoordinate;
1078 // Field-off and exact measurements isolate the accumulated energy loss.
1079 for (uint8_t row = 0; row < 5; ++row) {
1080 for (uint8_t column = 0; column < row; ++column) {
1081 state.covariance[packedCovarianceIndex(row, column)] = 0.f;
1082 }
1083 }
1084 auto linRef = diskLinRef(state);
1085 const float tanl = state.parameters[3];
1086 const float momentumScale = std::sqrt(1.f + tanl * tanl);
1087 float expectedMomentum = momentumScale / std::abs(state.parameters[4]);
1088 const float initialMomentum = expectedMomentum;
1089 constexpr float expectedSurfaceX0 = 0.0084f;
1090 const float pathX0 = expectedSurfaceX0 * momentumScale / std::abs(tanl);
1091 const material::IntegratedMaterialBudget expectedMaterial{
1093 std::array<detail::RefitMeasurementSlot, MFTNLayers> slots{};
1094 for (int hit = 0; hit < MFTNLayers; ++hit) {
1095 const auto layer = static_cast<uint16_t>(alongMomentum ? hit : MFTNLayers - 1 - hit);
1096 auto& slot = slots[hit];
1097 slot.surface = LayerId{layer};
1098 slot.present = true;
1099 const float z = kMFTSurfaces[layer].referenceCoordinate;
1100 const float transverseDistance = (z - state.referenceCoordinate) / tanl;
1101 slot.measurement.frame = {z,
1102 state.parameters[0] + transverseDistance * std::cos(state.parameters[2]),
1103 state.parameters[1] + transverseDistance * std::sin(state.parameters[2]), 0.f};
1104 slot.measurement.covariance = {0.04f, 0.f, 0.04f};
1105
1106 float resultMomentum = 0.f;
1107 float resultTheta2 = 0.f;
1108 float resultVariance = 0.f;
1109 const bool result = material::calculateMaterialPhysics(expectedMomentum, state.pid, state.absCharge,
1110 direction, expectedMaterial, resultMomentum, resultTheta2, resultVariance);
1111 BOOST_REQUIRE(result);
1112 expectedMomentum = resultMomentum;
1113 }
1114 float chi2 = 0.f;
1115 uint32_t acceptedHitCount = 0;
1116
1117 BOOST_REQUIRE(detail::driveRefitLeg(state, linRef, chi2, acceptedHitCount, slots, catalog, 0.f,
1118 direction, false, 100.f));
1119 BOOST_CHECK_EQUAL(acceptedHitCount, MFTNLayers);
1120 BOOST_CHECK_CLOSE(momentumScale / std::abs(state.parameters[4]), expectedMomentum, 1.e-4f);
1121 BOOST_CHECK(alongMomentum ? expectedMomentum < initialMomentum : expectedMomentum > initialMomentum);
1122 }
1123}
1124
1125// --- 10/11: chi2-gate failure and atomicity ----------------------------------
1126
1127BOOST_AUTO_TEST_CASE(NegativeMeasurementVarianceFailsTransactionally)
1128{
1129 for (const bool forward : {false, true}) {
1130 for (const bool negativeU : {false, true}) {
1131 // Cover both a negative residual variance and a small invalid measurement
1132 // variance hidden by the positive track covariance.
1133 for (const float variance : {-1.f, -1.e-6f}) {
1134 BOOST_TEST_CONTEXT("forward=" << forward << ", negativeU=" << negativeU << ", variance=" << variance)
1135 {
1136 auto state = forward ? diskState() : barrelState();
1137 const auto before = state;
1138 auto measurement = forward ? diskMeasurement() : barrelMeasurement();
1139 (negativeU ? measurement.covariance.uu : measurement.covariance.vv) = variance;
1140 float chi2 = 123.f;
1141
1142 BOOST_CHECK(!(forward ? Propagator::predictedChi2Forward(state, measurement, chi2)
1143 : Propagator::predictedChi2Barrel(state, measurement, chi2)));
1144 BOOST_CHECK_EQUAL(chi2, 123.f);
1145 BOOST_CHECK(!(forward ? Propagator::updateForward(state, measurement, chi2)
1146 : Propagator::updateBarrel(state, measurement, chi2)));
1147 BOOST_CHECK(bitEqual(state, before));
1148 BOOST_CHECK_EQUAL(chi2, 123.f);
1149 }
1150 }
1151 }
1152 }
1153}
1154
1155BOOST_AUTO_TEST_CASE(Chi2GateRejectsOversizedPredictedChi2Transactionally)
1156{
1157 auto state = barrelState();
1158 auto linRef = barrelLinRef(state);
1159 const auto poisonState = state;
1160 const auto poisonRef = linRef;
1161 auto measurement = barrelMeasurement();
1162 measurement.frame.u += 5.f; // far outlier vs the state's predicted local Y
1163 const auto descriptor = cylinderDescriptor(NominalSurfaceMaterial{0.f, 0.f});
1164 float chi2 = 0.f;
1165 const float poisonChi2 = chi2;
1166
1167 const bool ok = Propagator::propagateToMeasurement(state, linRef, descriptor, measurement, BarrelBz,
1168 material::MaterialTraversalDirection::AlongMomentum,
1169 true, 1.e-6f, chi2, false);
1170 BOOST_CHECK(!ok);
1171
1172 BOOST_CHECK(bitEqual(state, poisonState));
1173 BOOST_CHECK(bitEqual(linRef, poisonRef));
1174 BOOST_CHECK_EQUAL(chi2, poisonChi2);
1175}
1176
1177BOOST_AUTO_TEST_CASE(UnrecognizedTargetSurfaceKindFails)
1178{
1179 auto state = barrelState();
1180 auto linRef = barrelLinRef(state);
1181 const auto poisonState = state;
1182 const auto measurement = barrelMeasurement();
1183 SurfaceDescriptor descriptor = cylinderDescriptor(NominalSurfaceMaterial{0.f, 0.f});
1184 // SurfaceKind currently only has Cylinder/Disk (both recognized); this
1185 // proves the routing guard itself, not a reachable production input.
1186 descriptor.kind = static_cast<SurfaceKind>(0xFFu);
1187 float chi2 = 0.f;
1188
1189 const bool ok = Propagator::propagateToMeasurement(state, linRef, descriptor, measurement, BarrelBz,
1190 material::MaterialTraversalDirection::AlongMomentum,
1191 false, 0.f, chi2, false);
1192 BOOST_CHECK(!ok);
1193
1194 BOOST_CHECK(bitEqual(state, poisonState));
1195}
int32_t i
SurfaceTrackState state
float chi2
useful math constants
uint32_t j
Definition RawData.h:0
uint16_t pid
Definition RawData.h:2
std::array< float, NCoordinates > derivative
GLint GLenum GLint x
Definition glcorearb.h:403
GLuint64EXT * result
Definition glcorearb.h:5662
GLuint index
Definition glcorearb.h:781
GLsizei GLsizei GLchar * source
Definition glcorearb.h:798
GLint y
Definition glcorearb.h:270
GLint reference
Definition glcorearb.h:5487
GLsizei const GLfloat * value
Definition glcorearb.h:819
GLenum target
Definition glcorearb.h:1641
GLsizei const GLchar *const * path
Definition glcorearb.h:3591
GLenum GLuint GLint GLint layer
Definition glcorearb.h:1310
GLdouble GLdouble GLdouble z
Definition glcorearb.h:843
constexpr float Radl
Definition Constants.h:34
constexpr float Rho
Definition Constants.h:35
bool driveRefitLeg(SurfaceTrackState &state, SurfaceTrackParameters &linRef, float &chi2, uint32_t &acceptedHitCount, gsl::span< const RefitMeasurementSlot > orderedSlots, SurfaceCatalogView surfaceCatalog, float bz, material::MaterialTraversalDirection direction, bool shiftReferenceToMeasurement, float maxChi2) noexcept
bool calculateMaterialPhysics(float momentumGeV, o2::track::PID pid, uint8_t absCharge, MaterialTraversalDirection direction, IntegratedMaterialBudget material, float &momentumAfterGeV, float &highlandTheta2Rad2, float &relativeInverseMomentumVariance) noexcept
constexpr std::array< SurfaceDescriptor, MFTNLayers > kMFTSurfaces
constexpr int MFTNLayers
MFT CA half-disk layer count.
float arealDensityGPerCm2
crossed length*density, g/cm^2
float xOverX0
thickness in units of radiation length
std::map< std::string, ID > expected
BOOST_AUTO_TEST_CASE(ForwardHelixSmallAngleMomentumDerivative)
BOOST_CHECK(tree)
BOOST_CHECK_EQUAL(triggersD.size(), triggers.size())
std::vector< int > row