Project
Loading...
Searching...
No Matches
testMFTNormalizedRefit.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// Focused normalized-measurement authority and covariance coverage for the
13// descriptor-driven seed refit path.
14
15#define BOOST_TEST_MODULE ITSMFT MFTNormalizedRefit
16#define BOOST_TEST_MAIN
17#define BOOST_TEST_DYN_LINK
18
19#include <algorithm>
20#include <array>
21#include <cmath>
22#include <cstdint>
23#include <limits>
24#include <memory>
25#include <vector>
26
27#include <boost/test/unit_test.hpp>
28
34#include "MFTTracking/Constants.h"
35
36using namespace o2::itsmft::tracking;
37
38namespace
39{
40
41constexpr int NLayers = o2::mft::constants::mft::LayersNumber;
42// Field-off exercises the native linear propagation model deterministically.
43constexpr float Bz = 0.f;
44constexpr float DefaultSigma2 = 2.5e-7f; // (~0.5 micron)^2, MFT-scale resolution
45
46SurfaceTrackState makeDiskRefitStateFixture(
47 const SurfaceMeasurement& inner, const SurfaceMeasurement& outer,
48 float trackletMinPt)
49{
50 const float dx = outer.frame.u - inner.frame.u;
51 const float dy = outer.frame.v - inner.frame.v;
52 const float transverseLength = std::hypot(dx, dy);
53 const float qOverPt = trackletMinPt > 0.f ? 1.f / trackletMinPt : 0.f;
54
57 state.parameters[0] = outer.frame.u;
58 state.parameters[1] = outer.frame.v;
59 state.parameters[2] = std::atan2(dy, dx);
60 state.parameters[3] = (outer.frame.q - inner.frame.q) / transverseLength;
61 state.parameters[4] = qOverPt;
62 state.covariance[packedCovarianceIndex(0, 0)] = outer.covariance.uu;
63 state.covariance[packedCovarianceIndex(1, 0)] = outer.covariance.uv;
64 state.covariance[packedCovarianceIndex(1, 1)] = outer.covariance.vv;
65 state.covariance[packedCovarianceIndex(2, 2)] = 1.f;
66 state.covariance[packedCovarianceIndex(3, 3)] = 1.f;
67 const float qOverPtSigma = std::clamp(std::abs(qOverPt), 1.f, 10.f);
68 state.covariance[packedCovarianceIndex(4, 4)] = qOverPtSigma * qOverPtSigma;
69 state.kind = SurfaceKind::Disk;
70 state.absCharge = 1;
71 state.pid = o2::track::PID::Pion;
72 return state;
73}
74
75// A straight track through every MFT disk.
76struct StraightTrackGeometry {
77 std::array<float, NLayers> x{};
78 std::array<float, NLayers> y{};
79 std::array<float, NLayers> z{};
80 float xSlope{};
81
82 explicit StraightTrackGeometry(float slope) : xSlope(slope)
83 {
85 const float z0 = zLayer[0];
86 for (int layer = 0; layer < NLayers; ++layer) {
87 z[layer] = zLayer[layer];
88 x[layer] = 1.f + xSlope * (z[layer] - z0);
89 y[layer] = 0.5f - 0.006f * (z[layer] - z0);
90 }
91 }
92};
93
94// Owns one normalized refit fixture.
95struct RefitFixture {
96 std::array<std::vector<SurfaceMeasurement>, NLayers> storage;
97 std::array<std::vector<GlobalMeasurement>, NLayers> globalStorage;
98 std::vector<gsl::span<const GlobalMeasurement>> layerGlobals = std::vector<gsl::span<const GlobalMeasurement>>(NLayers);
99 std::vector<SurfaceDescriptor> catalogSurfaces;
100 SurfaceCatalogView catalog{};
101 TimeFrame frame;
102 TrackSeed seed;
104 int nHitLayers{0};
105
106 explicit RefitFixture(const StraightTrackGeometry& geometry, int hits = NLayers)
107 : nHitLayers(hits)
108 {
109 params.MinTrackLength = 5;
110 params.MinPt.assign(NLayers + 1, 0.f);
111 params.MaxChi2NDF = 30.f;
112
113 catalogSurfaces.resize(NLayers);
114 for (int layer = 0; layer < NLayers; ++layer) {
115 catalogSurfaces[layer].detectorSurfaceIndex = static_cast<uint16_t>(layer);
116 catalogSurfaces[layer].kind = SurfaceKind::Disk;
117 catalogSurfaces[layer].material = NominalSurfaceMaterial{0.f, 0.f};
118 }
119 catalog = SurfaceCatalogView{catalogSurfaces.data(), static_cast<uint32_t>(catalogSurfaces.size())};
120 BOOST_REQUIRE(frame.configure(DetectorConfiguration{catalogSurfaces}, 0, 0,
121 std::make_shared<BoundedMemoryResource>()));
122
123 uint16_t mask = 0;
124 for (int layer = 0; layer < hits; ++layer) {
125 setMeasurement(layer, geometry.x[layer], geometry.y[layer], geometry.z[layer],
126 DefaultSigma2, DefaultSigma2);
127 seed.getClusters()[layer] = 0;
128 mask |= static_cast<uint16_t>(uint16_t(1) << layer);
129 }
130 seed.setHitLayerMask(LayerMask{mask});
131
132 // The native driver starts from the CA seed state.
133 const int innerLayer = 0;
134 const int outerLayer = hits - 1;
135 seed.state() = makeDiskRefitStateFixture(
136 storage[innerLayer][0], storage[outerLayer][0], params.TrackletMinPt);
137 }
138
139 void setMeasurement(int layer, float x, float y, float z, float uu, float vv, float uv = 0.f)
140 {
142 // Disk measurements propagate to frame.q, their global z coordinate.
143 m.frame = {z, x, y, 0.f};
144 m.covariance.uu = uu;
145 m.covariance.vv = vv;
146 m.covariance.uv = uv;
147 GlobalMeasurement global{};
148 global.position = {x, y, z};
149 global.radius = std::hypot(x, y);
150 global.covariance = {uu, uv, 0.f, vv, 0.f, 0.f};
151 global.clusterId = 0u;
152 storage[layer].assign(1, m);
153 globalStorage[layer].assign(1, global);
154 layerGlobals[layer] = globalStorage[layer];
155 }
156
157 void syncFrame()
158 {
159 frame.resetTimeFrame();
160 for (int layer = 0; layer < NLayers; ++layer) {
161 for (std::size_t cluster = 0; cluster < globalStorage[layer].size(); ++cluster) {
162 frame.addMeasurement(LayerId{static_cast<uint16_t>(layer)}, globalStorage[layer][cluster],
163 storage[layer][cluster]);
164 }
165 }
166 }
167};
168
169bool refit(RefitFixture& fixture, TrackingCandidate& candidate)
170{
171 fixture.syncFrame();
172 SurfaceTrackState innerState{};
173 SurfaceTrackState outerState{};
174 float chi2 = 0.f;
175
176 if (!fitTrackSeedLegs(fixture.seed, fixture.frame, fixture.layerGlobals, fixture.catalog, Bz,
177 fixture.params.ShiftRefToCluster, fixture.params.MaxChi2ClusterAttachment,
178 fixture.params.MaxChi2NDF, fixture.params.RepeatRefitOut,
179 gsl::span<const float>(fixture.params.MinPt),
180 innerState, outerState, chi2)) {
181 return false;
182 }
183 candidate.seed = fixture.seed;
184 candidate.track.innerState = innerState;
185 candidate.track.outerState = outerState;
186 candidate.track.chi2 = chi2;
187 return true;
188}
189
190void checkTrackUnchanged(const TrackingCandidate& before, const TrackingCandidate& after)
191{
192 BOOST_CHECK_EQUAL(before.seed.getHitLayerMask().value(), after.seed.getHitLayerMask().value());
193 for (int position = 0; position < TrackSeed::MaxSurfaces; ++position) {
194 BOOST_CHECK_EQUAL(before.seed.getCluster(position), after.seed.getCluster(position));
195 }
196 for (int i = 0; i < 5; ++i) {
199 }
200 for (int i = 0; i < 15; ++i) {
203 }
206 BOOST_CHECK_EQUAL(static_cast<int>(before.track.innerState.kind), static_cast<int>(after.track.innerState.kind));
207 BOOST_CHECK_EQUAL(static_cast<int>(before.track.outerState.kind), static_cast<int>(after.track.outerState.kind));
208 BOOST_CHECK_EQUAL(before.track.chi2, after.track.chi2);
209}
210
211} // namespace
212
213// --- Normalized data drives the output --------------------------------------
214
215BOOST_AUTO_TEST_CASE(NormalizedGlobalCoordinateChangeAltersOutput)
216{
217 const StraightTrackGeometry geometry(0.3f);
218
219 RefitFixture reference(geometry);
220 TrackingCandidate referenceTrack;
221 BOOST_REQUIRE(refit(reference, referenceTrack));
222
223 // Perturb only the normalized global.x of one interior layer -- legacy
224 // backfill is absent (never populated) in both fixtures, so this isolates
225 // the normalized measurement as the sole cause of the changed outcome. The
226 // shift is far larger than DefaultSigma2's resolution, so the previously
227 // ~0 chi2/ndf now certainly exceeds MaxChi2NDF.
228 RefitFixture perturbed(geometry);
229 auto perturbedMeasurement = perturbed.storage[5].front();
230 perturbedMeasurement.frame.u += 0.05f;
231 perturbed.storage[5].assign(1, perturbedMeasurement);
232
233 TrackingCandidate perturbedTrack;
234 const bool perturbedOk = refit(perturbed, perturbedTrack);
235 BOOST_CHECK(!perturbedOk);
236}
237
238BOOST_AUTO_TEST_CASE(NormalizedCovarianceChangeAltersOutput)
239{
240 const StraightTrackGeometry geometry(0.3f);
241
242 RefitFixture reference(geometry);
243 TrackingCandidate referenceTrack;
244 BOOST_REQUIRE(refit(reference, referenceTrack));
245
246 // Scale up every layer's diagonal covariance uniformly (legacy backfill
247 // again absent in both fixtures): with exact-colinear points the fitted
248 // position/chi2 are unaffected, but the posterior parameter covariance the
249 // Kalman filter propagates is not -- a strictly larger measurement variance
250 // must not shrink the output covariance. This is a generic Kalman-filter
251 // property, unaffected by which per-hit update formula produces it.
252 RefitFixture loose(geometry);
253 for (int layer = 0; layer < NLayers; ++layer) {
254 auto m = loose.storage[layer].front();
255 m.covariance.uu *= 400.f;
256 m.covariance.vv *= 400.f;
257 loose.storage[layer].assign(1, m);
258 }
259 TrackingCandidate looseTrack;
260 BOOST_REQUIRE(refit(loose, looseTrack));
261
262 BOOST_CHECK_GT(looseTrack.track.outerState.covariance[packedCovarianceIndex(0, 0)],
263 referenceTrack.track.outerState.covariance[packedCovarianceIndex(0, 0)]);
264 BOOST_CHECK_GT(looseTrack.track.outerState.covariance[packedCovarianceIndex(1, 1)],
265 referenceTrack.track.outerState.covariance[packedCovarianceIndex(1, 1)]);
266}
267
268// --- C. Invalid normalized input fails cleanly, destination untouched -------
269
270BOOST_AUTO_TEST_CASE(NonFiniteSurfaceCoordinateFailsCleanly)
271{
272 const StraightTrackGeometry geometry(0.3f);
273 RefitFixture fx(geometry);
274 auto m = fx.storage[3].front();
275 m.frame.u = std::numeric_limits<float>::quiet_NaN();
276 fx.storage[3].assign(1, m);
277
278 TrackingCandidate before;
279 TrackingCandidate track = before;
280 BOOST_CHECK(!refit(fx, track));
281 checkTrackUnchanged(before, track);
282}
283
284BOOST_AUTO_TEST_CASE(InfiniteSurfaceCoordinateFailsCleanly)
285{
286 const StraightTrackGeometry geometry(0.3f);
287 RefitFixture fx(geometry);
288 auto m = fx.storage[3].front();
289 m.frame.q = std::numeric_limits<float>::infinity();
290 fx.storage[3].assign(1, m);
291
292 TrackingCandidate before;
293 TrackingCandidate track = before;
294 BOOST_CHECK(!refit(fx, track));
295 checkTrackUnchanged(before, track);
296}
297
298BOOST_AUTO_TEST_CASE(NonFiniteCovarianceFailsCleanly)
299{
300 const StraightTrackGeometry geometry(0.3f);
301 RefitFixture fx(geometry);
302 auto m = fx.storage[3].front();
303 m.covariance.uu = std::numeric_limits<float>::quiet_NaN();
304 fx.storage[3].assign(1, m);
305
306 TrackingCandidate before;
307 TrackingCandidate track = before;
308 BOOST_CHECK(!refit(fx, track));
309 checkTrackUnchanged(before, track);
310}
311
312BOOST_AUTO_TEST_CASE(NegativeCovarianceFailsCleanly)
313{
314 const StraightTrackGeometry geometry(0.3f);
315 RefitFixture fx(geometry);
316 auto m = fx.storage[3].front();
317 m.covariance.vv = -1.f;
318 fx.storage[3].assign(1, m);
319
320 TrackingCandidate before;
321 TrackingCandidate track = before;
322 BOOST_CHECK(!refit(fx, track));
323 checkTrackUnchanged(before, track);
324}
325
326BOOST_AUTO_TEST_CASE(OutOfRangeClusterIndexFailsCleanly)
327{
328 const StraightTrackGeometry geometry(0.3f);
329 RefitFixture fx(geometry);
330 fx.seed.getClusters()[3] = 99; // storage[3] only ever has one element (index 0)
331
332 TrackingCandidate before;
333 TrackingCandidate track = before;
334 BOOST_CHECK(!refit(fx, track));
335 checkTrackUnchanged(before, track);
336}
337
338BOOST_AUTO_TEST_CASE(InvalidClusterRefFailsCleanly)
339{
340 const StraightTrackGeometry geometry(0.3f);
341 RefitFixture fx(geometry);
342 auto m = fx.globalStorage[3].front();
343 m.clusterId = std::numeric_limits<uint32_t>::max();
344 fx.globalStorage[3].assign(1, m);
345 fx.layerGlobals[3] = fx.globalStorage[3];
346
347 TrackingCandidate before;
348 TrackingCandidate track = before;
349 BOOST_CHECK(!refit(fx, track));
350 checkTrackUnchanged(before, track);
351}
352
353BOOST_AUTO_TEST_CASE(RefitRejectsInvalidSurfaceCountsWithoutChangingOutput)
354{
355 RefitFixture fixture(StraightTrackGeometry{0.3f});
356 fixture.syncFrame();
357 for (const std::size_t count : {std::size_t{0}, std::size_t{MaxLayoutSurfaces + 1}}) {
358 std::vector<gsl::span<const GlobalMeasurement>> layers(count);
359 TrackingCandidate before;
360 before.track.innerState = fixture.seed.state();
361 before.track.outerState = fixture.seed.state();
362 before.track.chi2 = 123.f;
363 auto after = before;
364
365 BOOST_CHECK(!fitTrackSeedLegs(fixture.seed, fixture.frame, layers, fixture.catalog, Bz,
366 fixture.params.ShiftRefToCluster, fixture.params.MaxChi2ClusterAttachment,
367 fixture.params.MaxChi2NDF, true, fixture.params.MinPt,
368 after.track.innerState, after.track.outerState, after.track.chi2));
369
370 checkTrackUnchanged(before, after);
371 }
372}
373
374BOOST_AUTO_TEST_CASE(RefitBufferHandlesMaximumLayoutAndRepeatedLegs)
375{
376 RefitFixture fixture(StraightTrackGeometry{0.3f});
377 for (const bool repeat : {false, true}) {
378 fixture.params.RepeatRefitOut = repeat;
379 fixture.layerGlobals.resize(NLayers);
380 TrackingCandidate compact;
381 BOOST_REQUIRE(refit(fixture, compact));
382 // Additional absent surfaces must neither overflow the bounded buffer
383 // nor retain a measurement from the preceding refit leg.
384 fixture.layerGlobals.resize(MaxLayoutSurfaces);
385 TrackingCandidate maximum;
386 BOOST_REQUIRE(refit(fixture, maximum));
387 checkTrackUnchanged(compact, maximum);
388 }
389}
390
391// --- D. Preservation ---------------------------------------------------------
392
393BOOST_AUTO_TEST_CASE(PreservesSeedMembershipForGenericRefit)
394{
395 const StraightTrackGeometry geometry(0.3f);
396 // Holes at layers 2 and 7: MinTrackLength(5) <= 8 remaining hits.
397 RefitFixture fx(geometry);
398 fx.seed.getClusters()[2] = o2::its::constants::UnusedIndex;
399 fx.seed.getClusters()[7] = o2::its::constants::UnusedIndex;
400 LayerMask mask = fx.seed.getHitLayerMask();
401 mask.reset(2);
402 mask.reset(7);
403 fx.seed.setHitLayerMask(mask);
404
405 TrackingCandidate track;
406 BOOST_REQUIRE(refit(fx, track));
407
408 BOOST_CHECK_EQUAL(track.getNumberOfClusters(), NLayers - 2);
409 for (int layer = 0; layer < NLayers; ++layer) {
410 if (layer == 2 || layer == 7) {
412 BOOST_CHECK(!track.seed.hasCluster(layer));
413 } else {
414 BOOST_CHECK_EQUAL(track.getClusterIndex(layer), 0);
415 BOOST_CHECK(track.seed.hasCluster(layer));
416 }
417 }
418}
419
420// The native update uses the full uu/uv/vv measurement covariance.
421BOOST_AUTO_TEST_CASE(OffDiagonalCovarianceIsUsedByNativeUpdate)
422{
423 const StraightTrackGeometry geometry(0.3f);
424
425 RefitFixture reference(geometry);
426 TrackingCandidate referenceTrack;
427 BOOST_REQUIRE(refit(reference, referenceTrack));
428
429 RefitFixture withUv(geometry);
430 // A generous per-hit/per-track chi2 gate: this test's goal is only to
431 // prove a physically valid off-diagonal correlation changes the native
432 // update's output, not to probe chi2-gate behavior -- a nonzero
433 // correlation legitimately raises the predicted chi2 against a reference
434 // fit tuned for the uncorrelated (uv == 0) case.
435 withUv.params.MaxChi2ClusterAttachment = 1.e4f;
436 withUv.params.MaxChi2NDF = 1.e4f;
437 for (int layer = 0; layer < NLayers; ++layer) {
438 auto m = withUv.storage[layer].front();
439 // A modest, physically valid correlation (|coefficient| << 1) suffices
440 // to prove the point.
441 m.covariance.uv = 0.05f * std::sqrt(m.covariance.uu * m.covariance.vv);
442 withUv.storage[layer].assign(1, m);
443 }
444 TrackingCandidate withUvTrack;
445 BOOST_REQUIRE(refit(withUv, withUvTrack));
446
447 BOOST_CHECK_NE(withUvTrack.track.outerState.covariance[packedCovarianceIndex(0, 0)],
448 referenceTrack.track.outerState.covariance[packedCovarianceIndex(0, 0)]);
449}
450
451// --- Regression: stable pre-sort seed-cluster identity ---------------------
452
453BOOST_AUTO_TEST_CASE(GenericRefitUsesStablePreSortClusterIdentity)
454{
455 // Every hit layer has sorted seed index zero pointing at pre-sort cluster
456 // ID one. The generic refit must use that stable ID to retrieve the matching
457 // SurfaceMeasurement, rather than treating the sorted position as the ID.
458 const StraightTrackGeometry geometry(0.3f);
459 std::array<std::vector<SurfaceMeasurement>, NLayers> storage;
460 std::array<std::vector<GlobalMeasurement>, NLayers> globalStorage;
461 std::vector<gsl::span<const GlobalMeasurement>> layerGlobals = std::vector<gsl::span<const GlobalMeasurement>>(NLayers);
462 std::vector<SurfaceDescriptor> catalogSurfaces(NLayers);
463 for (int layer = 0; layer < NLayers; ++layer) {
464 catalogSurfaces[layer].kind = SurfaceKind::Disk;
465 catalogSurfaces[layer].material = NominalSurfaceMaterial{0.f, 0.f};
466 }
467 SurfaceCatalogView catalog{catalogSurfaces.data(), static_cast<uint32_t>(catalogSurfaces.size())};
468 TrackSeed seed;
471 params.MinPt.assign(NLayers + 1, 0.f);
472 params.MaxChi2NDF = 30.f;
473
474 uint16_t mask = 0;
475 for (int layer = 0; layer < NLayers; ++layer) {
477 m.frame = {geometry.z[layer], geometry.x[layer], geometry.y[layer], 0.f};
478 m.covariance.uu = DefaultSigma2;
479 m.covariance.vv = DefaultSigma2;
480 auto distractor = m;
481 distractor.covariance.uu = std::numeric_limits<float>::quiet_NaN();
482 GlobalMeasurement global{};
483 global.position = {geometry.x[layer], geometry.y[layer], geometry.z[layer]};
484 global.radius = std::hypot(geometry.x[layer], geometry.y[layer]);
485 global.covariance = {DefaultSigma2, 0.f, 0.f, DefaultSigma2, 0.f, 0.f};
486 global.clusterId = 1u;
487 auto distractorGlobal = global;
488 distractorGlobal.position.x += 100.f;
489 distractorGlobal.radius = std::hypot(distractorGlobal.position.x, distractorGlobal.position.y);
490 distractorGlobal.clusterId = 0u;
491 storage[layer] = {distractor, m};
492 globalStorage[layer] = {global, distractorGlobal};
493 layerGlobals[layer] = globalStorage[layer];
494
495 seed.getClusters()[layer] = 0;
496 mask |= static_cast<uint16_t>(uint16_t(1) << layer);
497 }
498 seed.setHitLayerMask(LayerMask{mask});
499
500 seed.state() = makeDiskRefitStateFixture(
501 storage[0][1], storage[NLayers - 1][1], params.TrackletMinPt);
502
503 TrackingCandidate track;
504 std::vector<std::vector<GlobalMeasurement>> globals(NLayers);
505 std::vector<std::vector<SurfaceMeasurement>> measurements(NLayers);
506 for (int layer = 0; layer < NLayers; ++layer) {
507 globals[layer] = globalStorage[layer];
508 measurements[layer] = storage[layer];
509 }
510 TimeFrame frame;
511 BOOST_REQUIRE(frame.configure(DetectorConfiguration{catalogSurfaces}, 0, 0,
512 std::make_shared<BoundedMemoryResource>()));
513 for (int layer = 0; layer < NLayers; ++layer) {
514 for (std::size_t cluster = 0; cluster < globals[layer].size(); ++cluster) {
515 frame.addMeasurement(LayerId{static_cast<uint16_t>(layer)}, globals[layer][cluster],
516 measurements[layer][cluster]);
517 }
518 }
519 SurfaceTrackState innerState{};
520 SurfaceTrackState outerState{};
521 float chi2 = 0.f;
522
523 BOOST_REQUIRE(fitTrackSeedLegs(seed, frame, layerGlobals, catalog, Bz,
524 params.ShiftRefToCluster, params.MaxChi2ClusterAttachment, params.MaxChi2NDF,
525 params.RepeatRefitOut, gsl::span<const float>(params.MinPt),
526 innerState, outerState, chi2));
527 track.seed = seed;
528 track.track.innerState = innerState;
529 track.track.outerState = outerState;
530 track.track.chi2 = chi2;
531
532 for (int layer = 0; layer < NLayers; ++layer) {
533 BOOST_CHECK(track.seed.hasCluster(layer));
534 BOOST_CHECK_EQUAL(track.getClusterIndex(layer), 0);
535 BOOST_CHECK_EQUAL(layerGlobals[layer][0].clusterId, 1u);
536 }
537}
538
539BOOST_AUTO_TEST_CASE(AllPointCircleRecoversSignedCurvatureAtDifferentLeverArms)
540{
541 // Exact helices exercise charge/field signs, rotations and the short
542 // transverse lever arm of a forward track without tuning to a noisy sample.
543 for (double bz : {-5., 5.}) {
544 for (double qOverPt : {-5., -1., -.05, .05, 1., 5.}) {
545 for (double phi : {-.7, 0., 1.8}) {
546 for (double scale : {0.01, 1.}) {
547 std::vector<detail::CircleFitPoint> points;
548 const double curvature = qOverPt * bz * o2::constants::math::B2C;
549 for (double arc : {2., 3., 4., 20., 25., 34., 40.}) {
550 arc *= scale;
551 const double x = std::sin(curvature * arc) / curvature;
552 const double y = 2 * std::pow(std::sin(curvature * arc / 2), 2) / curvature;
553 points.push_back({static_cast<float>(2 + x * std::cos(phi) - y * std::sin(phi)),
554 static_cast<float>(-1 + x * std::sin(phi) + y * std::cos(phi)), 1.e-6f, 2.e-7f, 2.e-6f});
555 }
556 const double fitted = detail::estimateCircleQOverPt(points, bz);
557 BOOST_REQUIRE(std::isfinite(fitted));
558 // Coordinate quantization is amplified as 1/leverArm^2 when
559 // recovering curvature. Bound it separately from fit arithmetic,
560 // which has a tighter same-input regression below.
561 double coordinateScale = 0.;
562 for (const auto& point : points) {
563 coordinateScale = std::max(coordinateScale, std::max(std::abs(double(point.x)), std::abs(double(point.y))));
564 }
565 const double dx = double(points.back().x) - points.front().x;
566 const double dy = double(points.back().y) - points.front().y;
567 const double quantizationTolerance = 8 * std::numeric_limits<float>::epsilon() * coordinateScale /
568 ((dx * dx + dy * dy) * std::abs(bz * o2::constants::math::B2C));
569 BOOST_CHECK_SMALL(fitted - qOverPt, quantizationTolerance + 2.e-6 * std::max(1., std::abs(qOverPt)));
570 }
571 }
572 }
573 }
574}
575
576BOOST_AUTO_TEST_CASE(AllPointCircleRejectsUnconstrainedOrInvalidInputs)
577{
578 std::array<detail::CircleFitPoint, 3> points{{{0.f, 0.f, 1.e-6f, 0.f, 1.e-6f},
579 {1.f, .01f, 1.e-6f, 0.f, 1.e-6f},
580 {2.f, .04f, 1.e-6f, 0.f, 1.e-6f}}};
581 BOOST_CHECK(std::isfinite(detail::estimateCircleQOverPt(points, 5.)));
583 BOOST_CHECK(!std::isfinite(detail::estimateCircleQOverPt(points, 0.)));
584 BOOST_CHECK(!std::isfinite(detail::estimateCircleQOverPt({points.data(), 2}, 5.)));
585 auto invalid = points;
586 invalid.back() = invalid.front();
587 BOOST_CHECK(!std::isfinite(detail::estimateCircleQOverPt(invalid, 5.)));
588 invalid = points;
589 invalid[1].xx = invalid[1].yy = 0.;
590 BOOST_CHECK(!std::isfinite(detail::estimateCircleQOverPt(invalid, 5.)));
591 invalid = points;
592 invalid[1].x = std::numeric_limits<float>::quiet_NaN();
593 BOOST_CHECK(!std::isfinite(detail::estimateCircleQOverPt(invalid, 5.)));
594}
595
596BOOST_AUTO_TEST_CASE(AllPointCirclePreservesCorrelatedWeightsUnderRotation)
597{
598 // Noisy points with distinct anisotropic errors exercise the weights;
599 // points on an exact circle would not constrain their covariance transform.
600 const std::array<detail::CircleFitPoint, 6> points{{{0.f, .0003f, 1.e-6f, 2.e-7f, 4.e-6f},
601 {1.f, .0012f, 5.e-6f, -5.e-7f, 1.e-6f},
602 {2.f, -.001f, 2.e-6f, 6.e-7f, 3.e-6f},
603 {4.f, -.0037f, 1.e-6f, -3.e-7f, 2.e-6f},
604 {8.f, -.0191f, 6.e-6f, 8.e-7f, 1.e-6f},
605 {12.f, -.051f, 2.e-6f, 4.e-7f, 5.e-6f}}};
606 // Original double-fit results evaluated on each rounded float input.
607 const std::array<double, 4> angles{-2.4, -.7, 0., 1.8};
608 const std::array<double, 4> references{0.57124524009151501, 0.57124303442230506,
609 0.5712510057031307, 0.57124927069565135};
610 for (std::size_t rotation = 0; rotation < angles.size(); ++rotation) {
611 const double angle = angles[rotation];
612 const double cs = std::cos(angle), sn = std::sin(angle);
613 auto rotated = points;
614 for (std::size_t i = 0; i < points.size(); ++i) {
615 const auto& point = points[i];
616 rotated[i] = {static_cast<float>(3. + cs * point.x - sn * point.y),
617 static_cast<float>(-2. + sn * point.x + cs * point.y),
618 static_cast<float>(cs * cs * point.xx - 2 * cs * sn * point.xy + sn * sn * point.yy),
619 static_cast<float>(cs * sn * point.xx + (cs * cs - sn * sn) * point.xy - cs * sn * point.yy),
620 static_cast<float>(sn * sn * point.xx + 2 * cs * sn * point.xy + cs * cs * point.yy)};
621 }
622 BOOST_CHECK_SMALL(detail::estimateCircleQOverPt(rotated, 5.f) - references[rotation], 1.e-6);
623 BOOST_CHECK_SMALL(detail::estimateCircleQOverPt(rotated, -5.f) + references[rotation], 1.e-6);
624 }
625}
626
627BOOST_AUTO_TEST_CASE(AllPointCircleBoundsCachedPoints)
628{
629 std::array<detail::CircleFitPoint, MaxLayoutSurfaces + 1> points;
630 const double curvature = 5. * o2::constants::math::B2C;
631 for (std::size_t i = 0; i < points.size(); ++i) {
632 const double arc = 1. + i;
633 points[i] = {static_cast<float>(std::sin(curvature * arc) / curvature),
634 static_cast<float>(2 * std::pow(std::sin(curvature * arc / 2), 2) / curvature),
635 1.e-6f, 0.f, 1.e-6f};
636 }
637 BOOST_CHECK_SMALL(detail::estimateCircleQOverPt({points.data(), MaxLayoutSurfaces}, 5.f) - 1.f, 2.e-6f);
638 BOOST_CHECK(!std::isfinite(detail::estimateCircleQOverPt(points, 5.)));
639}
640
641BOOST_AUTO_TEST_CASE(AllPointCirclePreservesWeakCurvatureInFloat)
642{
643 // Double-fit references for identical float inputs, at a 0.38 cm lever arm.
644 // This catches arithmetic cancellation independently of input quantization.
645 const std::array<double, 3> angles{-.7, 0., 1.8};
646 const std::array<std::array<double, 2>, 3> references{{{-0.049988686038833739, 0.050272146766691041},
647 {-0.049999995096480683, 0.049999995096480725},
648 {-0.050008335297723923, 0.050037910305885301}}};
649 for (std::size_t rotation = 0; rotation < angles.size(); ++rotation) {
650 for (int sign = 0; sign < 2; ++sign) {
651 const double curvature = (sign ? .05 : -.05) * 5 * o2::constants::math::B2C;
652 const double phi = angles[rotation];
653 std::array<detail::CircleFitPoint, 7> points;
654 for (std::size_t i = 0; i < points.size(); ++i) {
655 const double arc = (2. + 38. * i / 6) * .01;
656 const double x = std::sin(curvature * arc) / curvature;
657 const double y = 2 * std::pow(std::sin(curvature * arc / 2), 2) / curvature;
658 points[i] = {static_cast<float>(x * std::cos(phi) - y * std::sin(phi)),
659 static_cast<float>(x * std::sin(phi) + y * std::cos(phi)), 1.e-6f, 2.e-7f, 2.e-6f};
660 }
661 BOOST_CHECK_SMALL(detail::estimateCircleQOverPt(points, 5.f) - references[rotation][sign], 2.e-7);
662 }
663 }
664}
Passive common TimeFrame owner.
int32_t i
SurfaceTrackState state
float chi2
uint16_t slope
Definition RawData.h:1
GPU-portable whole-track seed for common CA tracking.
static constexpr int MaxSurfaces
Definition TrackSeed.h:41
GLint GLenum GLint x
Definition glcorearb.h:403
const GLfloat * m
Definition glcorearb.h:4066
GLint GLsizei count
Definition glcorearb.h:399
GLint y
Definition glcorearb.h:270
GLint reference
Definition glcorearb.h:5487
GLenum const GLfloat * params
Definition glcorearb.h:272
GLfloat angle
Definition glcorearb.h:4071
GLenum GLuint GLint GLint layer
Definition glcorearb.h:1310
GLint GLuint mask
Definition glcorearb.h:291
GLdouble GLdouble GLdouble z
Definition glcorearb.h:843
constexpr int UnusedIndex
Definition Constants.h:32
float estimateCircleQOverPt(gsl::span< const CircleFitPoint > points, float bz) noexcept
Definition RefitDriver.h:71
constexpr float MinCircleFitBz
Definition RefitDriver.h:41
bool fitTrackSeedLegs(const TrackSeed &seed, const TimeFrame &frame, gsl::span< const gsl::span< const GlobalMeasurement > > layerGlobals, SurfaceCatalogView surfaceCatalog, float bz, bool shiftReferenceToMeasurement, float maxChi2ClusterAttachment, float maxChi2NDF, bool repeatRefitOut, gsl::span< const float > minPt, SurfaceTrackState &outParamIn, SurfaceTrackState &outParamOut, float &outChi2) noexcept
constexpr uint32_t MaxLayoutSurfaces
Definition IdTypes.h:70
constexpr Int_t LayersNumber
Definition Constants.h:37
constexpr std::array< Float_t, LayersNumber > LayerZCoordinate()
Definition Constants.h:44
int MinTrackLength
General parameters.
void addMeasurement(LayerId surface, GlobalMeasurement global, const SurfaceMeasurement &measurement)
Definition TimeFrame.cxx:56
bool configure(DetectorConfiguration &&layout, std::size_t maxEdges, std::size_t maxCells, std::shared_ptr< BoundedMemoryResource > memoryPool)
BOOST_AUTO_TEST_CASE(NormalizedGlobalCoordinateChangeAltersOutput)
BOOST_CHECK(tree)
BOOST_CHECK_EQUAL(triggersD.size(), triggers.size())