Skip to content

Commit f1e2189

Browse files
committed
Speedup circle fit
1 parent aad0f92 commit f1e2189

2 files changed

Lines changed: 178 additions & 42 deletions

File tree

‎Detectors/ITSMFT/common/tracking/include/ITSMFTTracking/RefitDriver.h‎

Lines changed: 90 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -41,46 +41,100 @@ namespace detail
4141
constexpr float MinCircleFitBz = 0.01f; // kG
4242

4343
struct CircleFitPoint {
44-
double x, y;
45-
double xx, xy, yy;
44+
float x, y;
45+
float xx, xy, yy;
4646
};
4747

48-
// Fit y = a + b*x + c*(x*x + y*y) after translating, rotating and scaling
49-
// the attached hits. Iteratively project their xy covariance onto the circle
50-
// normal. Double precision is confined to this weak-bending seed estimate.
51-
inline double estimateCircleQOverPt(gsl::span<const CircleFitPoint> points, double bz) noexcept
48+
// Preserve cancellation in a*b-c*d with two fused multiply-add operations.
49+
inline float circleDifferenceOfProducts(float a, float b, float c, float d)
5250
{
53-
constexpr double invalid = std::numeric_limits<double>::quiet_NaN();
54-
if (points.size() < 3 || !std::isfinite(bz) || std::abs(bz) < MinCircleFitBz) {
51+
const float cd = c * d;
52+
return std::fma(a, b, -cd) + std::fma(-c, d, cd);
53+
}
54+
55+
struct CircleFloatDifference {
56+
float hi, lo;
57+
};
58+
59+
// Return the rounded difference and its residual; do not reassociate these sums.
60+
inline CircleFloatDifference circleTwoDiff(float a, float b)
61+
{
62+
const float hi = a - b;
63+
const float bv = a - hi;
64+
return {hi, (a - (hi + bv)) + (bv - b)};
65+
}
66+
67+
// Fit y = a + b*x + c*(x*x + y*y) in a frame centered on the chord.
68+
// Compensate coordinate differences and the chord determinant to preserve
69+
// the small sagitta in float. Cache invariant transforms for the four
70+
// covariance-reweighting iterations; all fit arithmetic is single precision.
71+
inline float estimateCircleQOverPt(gsl::span<const CircleFitPoint> points, float bz) noexcept
72+
{
73+
const float invalid = std::numeric_limits<float>::quiet_NaN();
74+
if (points.size() < 3 || points.size() > MaxLayoutSurfaces || std::abs(bz) < MinCircleFitBz) {
5575
return invalid;
5676
}
57-
const double x0 = points.front().x, y0 = points.front().y;
58-
const double dx = points.back().x - x0, dy = points.back().y - y0;
59-
const double length = std::hypot(dx, dy);
60-
if (!(length > 0.) || !std::isfinite(length)) {
77+
const float x0 = points.front().x, y0 = points.front().y;
78+
const auto dx = circleTwoDiff(points.back().x, x0);
79+
const auto dy = circleTwoDiff(points.back().y, y0);
80+
float lengthSquared = std::fma(dx.hi, dx.hi, dy.hi * dy.hi);
81+
lengthSquared += 2.f * std::fma(dx.hi, dx.lo, dy.hi * dy.lo);
82+
const float length = std::sqrt(lengthSquared);
83+
if (!(length > 0.f) || !std::isfinite(length)) {
6184
return invalid;
6285
}
63-
const double cs = dx / length, sn = dy / length;
64-
std::array<double, 3> fit{};
86+
const float cs = dx.hi / length, sn = dy.hi / length;
87+
const float invLengthSquared = 1.f / lengthSquared;
88+
struct CachedPoint {
89+
float x, y, r2, xx, xy, yy;
90+
};
91+
std::array<CachedPoint, MaxLayoutSurfaces> cache;
92+
for (std::size_t i = 0; i < points.size(); ++i) {
93+
const auto& in = points[i];
94+
const auto px = circleTwoDiff(in.x, x0);
95+
const auto py = circleTwoDiff(in.y, y0);
96+
// Retain subtraction residuals before dividing the small determinant.
97+
float cross = circleDifferenceOfProducts(dx.hi, py.hi, dy.hi, px.hi);
98+
float dot = std::fma(dx.hi, px.hi, dy.hi * py.hi);
99+
100+
float correction = std::fma(dx.hi, py.lo, dx.lo * py.hi);
101+
correction = std::fma(-dy.hi, px.lo, correction);
102+
correction = std::fma(-dy.lo, px.hi, correction);
103+
correction += circleDifferenceOfProducts(dx.lo, py.lo, dy.lo, px.lo);
104+
cross += correction;
105+
dot += std::fma(dx.hi, px.lo, std::fma(dx.lo, px.hi, std::fma(dy.hi, py.lo, dy.lo * py.hi)));
106+
107+
const float x = dot * invLengthSquared - .5f;
108+
const float y = cross * invLengthSquared;
109+
const float xx = in.xx, xy = in.xy, yy = in.yy;
110+
cache[i] = {x, y, std::fma(x, x, y * y),
111+
std::fma(cs * cs, xx, std::fma(2.f * cs * sn, xy, sn * sn * yy)) * invLengthSquared,
112+
std::fma(-cs * sn, xx, std::fma(std::fma(cs, cs, -sn * sn), xy, cs * sn * yy)) * invLengthSquared,
113+
std::fma(sn * sn, xx, std::fma(-2.f * cs * sn, xy, cs * cs * yy)) * invLengthSquared};
114+
}
115+
std::array<float, 3> fit{};
65116
for (int iteration = 0; iteration < 4; ++iteration) {
66-
double matrix[3][4]{};
67-
for (const auto& point : points) {
68-
const double x = ((point.x - x0) * cs + (point.y - y0) * sn) / length;
69-
const double y = (-(point.x - x0) * sn + (point.y - y0) * cs) / length;
70-
const double nx = -fit[1] - 2 * fit[2] * x, ny = 1 - 2 * fit[2] * y;
71-
const double gx = cs * nx - sn * ny, gy = sn * nx + cs * ny;
72-
const double variance = (gx * gx * point.xx + 2 * gx * gy * point.xy + gy * gy * point.yy) / (length * length);
73-
if (!(variance > 0.) || !std::isfinite(variance)) {
117+
float matrix[3][4]{};
118+
for (const auto& point : gsl::span<const CachedPoint>{cache.data(), points.size()}) {
119+
const float nx = std::fma(-2.f * fit[2], point.x, -fit[1]);
120+
const float ny = std::fma(-2.f * fit[2], point.y, 1.f);
121+
const float variance = std::fma(nx * nx, point.xx, std::fma(2.f * nx * ny, point.xy, ny * ny * point.yy));
122+
if (!(variance > 0.f) || !std::isfinite(variance)) {
74123
return invalid;
75124
}
76-
const double weight = 1 / variance, basis[3] = {1, x, x * x + y * y};
125+
126+
const float weight = 1.f / variance, basis[4] = {1.f, point.x, point.r2, point.y};
77127
for (int i = 0; i < 3; ++i) {
78-
for (int j = 0; j < 3; ++j) {
79-
matrix[i][j] += weight * basis[i] * basis[j];
128+
const float weighted = weight * basis[i];
129+
for (int j = i; j < 4; ++j) {
130+
matrix[i][j] = std::fma(weighted, basis[j], matrix[i][j]);
80131
}
81-
matrix[i][3] += weight * basis[i] * y;
82132
}
83133
}
134+
135+
matrix[1][0] = matrix[0][1];
136+
matrix[2][0] = matrix[0][2];
137+
matrix[2][1] = matrix[1][2];
84138
// Solve the three normal equations with partial pivoting.
85139
for (int i = 0; i < 3; ++i) {
86140
int pivot = i;
@@ -92,28 +146,29 @@ inline double estimateCircleQOverPt(gsl::span<const CircleFitPoint> points, doub
92146
for (int k = i; k < 4; ++k) {
93147
std::swap(matrix[i][k], matrix[pivot][k]);
94148
}
95-
const double diagonal = matrix[i][i];
96-
if (std::abs(diagonal) < 1.e-15) {
149+
const float diagonal = matrix[i][i];
150+
if (std::abs(diagonal) < 1.e-15f) {
97151
return invalid;
98152
}
99153
for (int k = i; k < 4; ++k) {
100154
matrix[i][k] /= diagonal;
101155
}
102156
for (int j = 0; j < 3; ++j) {
103-
if (j != i) {
104-
const double factor = matrix[j][i];
105-
for (int k = i; k < 4; ++k) {
106-
matrix[j][k] -= factor * matrix[i][k];
107-
}
157+
if (j == i) {
158+
continue;
159+
}
160+
const float factor = matrix[j][i];
161+
for (int k = i; k < 4; ++k) {
162+
matrix[j][k] = std::fma(-factor, matrix[i][k], matrix[j][k]);
108163
}
109164
}
110165
}
111166
for (int i = 0; i < 3; ++i) {
112167
fit[i] = matrix[i][3];
113168
}
114169
}
115-
const double discriminant = 1 + fit[1] * fit[1] - 4 * fit[0] * fit[2];
116-
return discriminant > 0 ? 2 * fit[2] / (length * std::sqrt(discriminant) * bz * o2::constants::math::B2C) : invalid;
170+
const float discriminant = std::fma(-4.f * fit[0], fit[2], std::fma(fit[1], fit[1], 1.f));
171+
return discriminant > 0.f ? 2.f * fit[2] / (length * std::sqrt(discriminant) * bz * o2::constants::math::B2C) : invalid;
117172
}
118173

119174
struct RefitMeasurementSlot {

‎Detectors/ITSMFT/common/tracking/test/testMFTNormalizedRefit.cxx‎

Lines changed: 88 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -550,12 +550,23 @@ BOOST_AUTO_TEST_CASE(AllPointCircleRecoversSignedCurvatureAtDifferentLeverArms)
550550
arc *= scale;
551551
const double x = std::sin(curvature * arc) / curvature;
552552
const double y = 2 * std::pow(std::sin(curvature * arc / 2), 2) / curvature;
553-
points.push_back({2 + x * std::cos(phi) - y * std::sin(phi),
554-
-1 + x * std::sin(phi) + y * std::cos(phi), 1.e-6, 2.e-7, 2.e-6});
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});
555555
}
556556
const double fitted = detail::estimateCircleQOverPt(points, bz);
557557
BOOST_REQUIRE(std::isfinite(fitted));
558-
BOOST_CHECK_SMALL(fitted - qOverPt, 1.e-7 * std::max(1., std::abs(qOverPt)));
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)));
559570
}
560571
}
561572
}
@@ -564,9 +575,9 @@ BOOST_AUTO_TEST_CASE(AllPointCircleRecoversSignedCurvatureAtDifferentLeverArms)
564575

565576
BOOST_AUTO_TEST_CASE(AllPointCircleRejectsUnconstrainedOrInvalidInputs)
566577
{
567-
std::array<detail::CircleFitPoint, 3> points{{{0., 0., 1.e-6, 0., 1.e-6},
568-
{1., .01, 1.e-6, 0., 1.e-6},
569-
{2., .04, 1.e-6, 0., 1.e-6}}};
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}}};
570581
BOOST_CHECK(std::isfinite(detail::estimateCircleQOverPt(points, 5.)));
571582
BOOST_CHECK(std::isfinite(detail::estimateCircleQOverPt(points, detail::MinCircleFitBz)));
572583
BOOST_CHECK(!std::isfinite(detail::estimateCircleQOverPt(points, 0.)));
@@ -578,6 +589,76 @@ BOOST_AUTO_TEST_CASE(AllPointCircleRejectsUnconstrainedOrInvalidInputs)
578589
invalid[1].xx = invalid[1].yy = 0.;
579590
BOOST_CHECK(!std::isfinite(detail::estimateCircleQOverPt(invalid, 5.)));
580591
invalid = points;
581-
invalid[1].x = std::numeric_limits<double>::quiet_NaN();
592+
invalid[1].x = std::numeric_limits<float>::quiet_NaN();
582593
BOOST_CHECK(!std::isfinite(detail::estimateCircleQOverPt(invalid, 5.)));
583594
}
595+
596+
BOOST_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+
627+
BOOST_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+
641+
BOOST_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+
}

0 commit comments

Comments
 (0)