Skip to content

Commit 54198c0

Browse files
committed
DCAFitter: apply X-error regularization only where it is needed, drop duplications
DCAFitter: apply X-error regularization only where it is needed, drop duplications Follow-up to PR15610. The fitted PCA and chi2 are unchanged; the vertex covariance returned by calcPCACovMatrix() (and hence by createParentTrackParCov()) becomes larger along weakly constrained directions. 1. TrackCovI::set() takes the regularization strength as an argument (XRegErrFactor by default, XRegNone to disable). The weak dummy X error added to sxx is needed only to keep the Newton Hessian invertible for (nearly) collinear prongs, so calcPCACovMatrix() now rebuilds the information matrices with XRegNone: there only their sum is inverted (and an ill-conditioned sum is already replaced by a loose dummy), hence the prior would only make the longitudinal vertex error follow the dummy XRegErrFactor*Cyy instead of the track slopes. Documented in DCAFitterN_derivation.md. 2. createParentTrackParCov() uses getCovXYZPxPyPzGlo() again instead of the hand-written momentum Jacobian, which was algebraically identical to it (same J for the native snp,tgl,q/pt parameters, same |q/pt|/|snp| guard). Also replaced the literal 21 by o2::track::kLabCovMatSize. 3. The M*E*M^T rotate-and-sum block, identical in calcInverseWeight() and calcPCACovMatrix(), is factored into addRotatedTrackInfo(). This replaces the now unused getTrackRotMatrix(). 4. calcCollinearInflation() uses o2::gpu::GPUCommonMath instead of host-only std::sqrt/clamp/max/min, so it stays device-compilable although currently not called; its commented-out call site becomes an explicit TODO and the unused InflateRelDet/MaxInflation constants are removed. 5. correctTracks() sets mPropFailed and logs via mLoggerBadProp on a failed propagation, as propagate{Param}ToX() do; the commented-out Taylor block is replaced by a comment on why mCandTr must be moved and why the analytic constant-Bz transport is used for the small Newton steps. 6. Added the missing newline to the mLoggerBadPCACov message. Verified by a full explicit instantiation of DCAFitterN<2>/<3>, which type-checks all members including the otherwise uninstantiated calcCollinearInflation(). Co-Authored-By: Claude Opus 5
1 parent 626c2ad commit 54198c0

2 files changed

Lines changed: 97 additions & 101 deletions

File tree

Common/DCAFitter/DCAFitterN_derivation.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -479,9 +479,11 @@ I_XX -> I_XX + 1/sigma_X,prior^2 .
479479
In code this is implemented as
480480

481481
```cpp
482-
constexpr float XRegErrFactor = 10.f;
483-
const float sigmaX2 = C_YY * XRegErrFactor;
484-
sxx += 1.f / sigmaX2;
482+
static constexpr float XRegErrFactor = 10.f;
483+
...
484+
if (xRegErrFactor > 0.f) {
485+
sxx += 1.f / (cyy * xRegErrFactor);
486+
}
485487
```
486488

487489
This is different from multiplying `I_XX` by a number below one. A reduction of
@@ -491,7 +493,15 @@ not alter `I_YY`, `I_YZ`, or `I_ZZ`.
491493

492494
The regularization should remain weak. It is a numerical stabilizer for badly
493495
conditioned geometries, not an additional detector measurement of the local
494-
track `X` coordinate.
496+
track `X` coordinate. For this reason it is applied **only** where an invertible
497+
single-track contribution is actually required, i.e. for the `I_i` entering the
498+
chi2 minimization (`mTrcEInv`, hence `calcInverseWeight()`, `calcPCACoefs()`,
499+
`calcChi2()` and the Newton Hessian). `calcPCACovMatrix()` rebuilds the `I_i`
500+
with `TrackCovI::XRegNone`: there the prior is not needed (the sum over prongs is
501+
inverted, not the individual terms, and a genuinely ill-conditioned sum is
502+
detected and replaced by a loose dummy covariance), and including it would make
503+
the reported longitudinal vertex error follow the dummy `XRegErrFactor * C_YY`
504+
instead of the track slopes.
495505

496506
### `calcPCACovMatrix()`
497507

Common/DCAFitter/include/DCAFitter/DCAFitterN.h

Lines changed: 83 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,22 @@ struct TrackCovI {
3636
// H = {{-dY/dX, 1, 0}, {-dZ/dX, 0, 1}}.
3737
float sxx, sxy, sxz, syy, syz, szz;
3838

39+
// H^T Cyz^{-1} H is singular by construction (rank 2): the chi2 is invariant
40+
// under sliding the reference point along the trajectory. A weak dummy X error
41+
// sigma_x^2 = XRegErrFactor * Cyy is added to the sxx element to regularize it.
42+
// This is needed ONLY to keep the Newton Hessian of the chi2 minimization
43+
// invertible for (nearly) collinear prongs.
44+
// It must NOT be used when the single track information matrices are summed to
45+
// obtain the PCA covariance (see calcPCACovMatrix): there the regularization
46+
// would define the longitudinal vertex error by this dummy term instead of by
47+
// the track slopes, i.e. reintroduce the very artifact it replaces. Pass
48+
// XRegNone in that case.
49+
static constexpr float XRegErrFactor = 10.f;
50+
static constexpr float XRegNone = -1.f;
51+
3952
GPUdDefault() TrackCovI() = default;
4053

41-
GPUd() bool set(const o2::track::TrackParCov& trc)
54+
GPUd() bool set(const o2::track::TrackParCov& trc, float xRegErrFactor = XRegErrFactor)
4255
{
4356
// Invert the 2D covariance of the measured track position (Y,Z).
4457
float cyy = trc.getSigmaY2(), czz = trc.getSigmaZ2(), cyz = trc.getSigmaZY();
@@ -59,10 +72,9 @@ struct TrackCovI {
5972
sxy = -(syy * dydx + syz * dzdx);
6073
sxz = -(syz * dydx + szz * dzdx);
6174
sxx = dydx * dydx * syy + 2.f * dydx * dzdx * syz + dzdx * dzdx * szz;
62-
// The matrix is degenerate by construction, regularize sxx term to preserve the original YZ block exactly
63-
constexpr float XRegErrFactor = 10.f;
64-
const float sigmaX2 = cyy * XRegErrFactor;
65-
sxx += 1.f / sigmaX2;
75+
if (xRegErrFactor > 0.f) { // regularize the sxx term only, this preserves the YZ block exactly
76+
sxx += 1.f / (cyy * xRegErrFactor);
77+
}
6678
return res;
6779
}
6880
};
@@ -315,14 +327,23 @@ class DCAFitterN
315327
///< track X-param at V0 candidate (no check for the candidate validity)
316328
GPUd() float getTrackX(int i, int cand = 0) const { return getTrackPos(i, cand)[0]; }
317329

318-
GPUd() MatStd3D getTrackRotMatrix(int i) const // generate 3D matrix for track rotation to global frame
330+
///< Accumulate the track information matrix rotated to the global frame, M*E*M^T, into the
331+
///< flat MatRepSym-ordered {XX,XY,YY,XZ,YZ,ZZ} accumulator. Shared by calcInverseWeight()
332+
///< (sum over prongs of a candidate) and calcPCACovMatrix() (same sum, unregularized).
333+
GPUd() static void addRotatedTrackInfo(double* arrmat, const TrackAuxPar& taux, const TrackCovI& tcov)
319334
{
320-
MatStd3D mat;
321-
mat(2, 2) = 1;
322-
mat(0, 0) = mat(1, 1) = mTrAux[i].c;
323-
mat(0, 1) = -mTrAux[i].s;
324-
mat(1, 0) = mTrAux[i].s;
325-
return mat;
335+
enum { XX,
336+
XY,
337+
YY,
338+
XZ,
339+
YZ,
340+
ZZ };
341+
arrmat[XX] += taux.cc * tcov.sxx - 2. * taux.cs * tcov.sxy + taux.ss * tcov.syy;
342+
arrmat[XY] += taux.cs * (tcov.sxx - tcov.syy) + (taux.cc - taux.ss) * tcov.sxy;
343+
arrmat[XZ] += taux.c * tcov.sxz - taux.s * tcov.syz;
344+
arrmat[YY] += taux.ss * tcov.sxx + 2. * taux.cs * tcov.sxy + taux.cc * tcov.syy;
345+
arrmat[YZ] += taux.s * tcov.sxz + taux.c * tcov.syz;
346+
arrmat[ZZ] += tcov.szz;
326347
}
327348

328349
GPUd() void assign(int) {}
@@ -525,21 +546,8 @@ GPUd() bool DCAFitterN<N, Args...>::calcInverseWeight()
525546
//< calculate [sum_{0<j<N} M_j*E_j*M_j^T]^-1 used for Ti matrices, see EQ.T
526547
auto* arrmat = mWeightInv.Array();
527548
memset(arrmat, 0, sizeof(mWeightInv));
528-
enum { XX,
529-
XY,
530-
YY,
531-
XZ,
532-
YZ,
533-
ZZ };
534-
for (int i = N; i--;) {
535-
const auto& taux = mTrAux[i];
536-
const auto& tcov = mTrcEInv[mCurHyp][i];
537-
arrmat[XX] += taux.cc * tcov.sxx - 2. * taux.cs * tcov.sxy + taux.ss * tcov.syy;
538-
arrmat[XY] += taux.cs * (tcov.sxx - tcov.syy) + (taux.cc - taux.ss) * tcov.sxy;
539-
arrmat[XZ] += taux.c * tcov.sxz - taux.s * tcov.syz;
540-
arrmat[YY] += taux.ss * tcov.sxx + 2. * taux.cs * tcov.sxy + taux.cc * tcov.syy;
541-
arrmat[YZ] += taux.s * tcov.sxz + taux.c * tcov.syz;
542-
arrmat[ZZ] += tcov.szz;
549+
for (int i = N; i--;) { // the mTrcEInv used here are regularized, see TrackCovI::XRegErrFactor
550+
addRotatedTrackInfo(arrmat, mTrAux[i], mTrcEInv[mCurHyp][i]);
543551
}
544552
// invert 3x3 symmetrix matrix
545553
return mWeightInv.Invert();
@@ -810,6 +818,8 @@ GPUd() void DCAFitterN<N, Args...>::calcPCANoErr()
810818
template <int N, typename... Args>
811819
GPUd() double DCAFitterN<N, Args...>::calcCollinearInflation(int cand) const
812820
{
821+
// Note: only std::array and o2::gpu::GPUCommonMath are used here, no host-only <algorithm>/<cmath>,
822+
// so that the method stays compilable for the device even though it is currently not called.
813823
std::array<std::array<double, 3>, N> u{};
814824
int nu = 0;
815825

@@ -818,11 +828,11 @@ GPUd() double DCAFitterN<N, Args...>::calcCollinearInflation(int cand) const
818828
if (!getTrack(i, cand).getPxPyPzGlo(p)) {
819829
continue;
820830
}
821-
const double p2 = p[0] * p[0] + p[1] * p[1] + p[2] * p[2];
822-
if (p2 <= 0.) {
831+
const float p2 = p[0] * p[0] + p[1] * p[1] + p[2] * p[2]; // float: GPUCommonMath::Sqrt is float-only and p is float anyway
832+
if (p2 <= 0.f) {
823833
continue;
824834
}
825-
const double pI = 1. / std::sqrt(p2);
835+
const double pI = 1. / o2::gpu::GPUCommonMath::Sqrt(p2);
826836
u[nu++] = {p[0] * pI, p[1] * pI, p[2] * pI};
827837
}
828838

@@ -835,8 +845,8 @@ GPUd() double DCAFitterN<N, Args...>::calcCollinearInflation(int cand) const
835845
for (int i = 0; i < nu; ++i) {
836846
for (int j = i + 1; j < nu; ++j) {
837847
double cij = u[i][0] * u[j][0] + u[i][1] * u[j][1] + u[i][2] * u[j][2];
838-
cij = std::clamp(cij, -1., 1.);
839-
sin2Mean += std::max(0., 1. - cij * cij);
848+
cij = o2::gpu::GPUCommonMath::Clamp(cij, -1., 1.);
849+
sin2Mean += o2::gpu::GPUCommonMath::Max(0., 1. - cij * cij);
840850
++npairs;
841851
}
842852
}
@@ -847,7 +857,7 @@ GPUd() double DCAFitterN<N, Args...>::calcCollinearInflation(int cand) const
847857
if (sin2Mean <= 0.) {
848858
return MaxInflation;
849859
}
850-
return sin2Mean < Sin2Ref ? std::min(MaxInflation, Sin2Ref / sin2Mean) : 1.;
860+
return sin2Mean < Sin2Ref ? o2::gpu::GPUCommonMath::Min(MaxInflation, Sin2Ref / sin2Mean) : 1.;
851861
}
852862

853863
//___________________________________________________________________
@@ -857,48 +867,41 @@ GPUd() o2::math_utils::SMatrix<double, 3, 3, o2::math_utils::MatRepSym<double, 3
857867
// Each track measures Y and Z at the vertex X. With the local slopes
858868
// sy = dY/dX and sz = dZ/dX, its vertex measurement matrix is
859869
// H = {{-sy, 1, 0}, {-sz, 0, 1}}. The longitudinal information must come
860-
// from the track geometry, not from a dummy X variance.
870+
// from the track geometry, not from a dummy X variance: hence the per-track
871+
// information matrices are built here WITHOUT the sxx regularization used by
872+
// the minimization (TrackCovI::XRegNone), otherwise the vertex error along the
873+
// weakly constrained direction would be defined by that dummy term.
874+
// A singular/ill-conditioned sum is caught below and replaced by a loose dummy.
861875
MatSym3D info;
862876
auto* arrmat = info.Array();
863877
memset(arrmat, 0, sizeof(info));
864-
enum { XX,
865-
XY,
866-
YY,
867-
XZ,
868-
YZ,
869-
ZZ };
870878
const int ord = mOrder[cand];
871879
for (int i = N; i--;) {
872-
const auto& taux = mTrAux[i];
873880
TrackCovI tcov;
874-
tcov.set(mCandTr[ord][i]);
875-
arrmat[XX] += taux.cc * tcov.sxx - 2. * taux.cs * tcov.sxy + taux.ss * tcov.syy;
876-
arrmat[XY] += taux.cs * (tcov.sxx - tcov.syy) + (taux.cc - taux.ss) * tcov.sxy;
877-
arrmat[XZ] += taux.c * tcov.sxz - taux.s * tcov.syz;
878-
arrmat[YY] += taux.ss * tcov.sxx + 2. * taux.cs * tcov.sxy + taux.cc * tcov.syy;
879-
arrmat[YZ] += taux.s * tcov.sxz + taux.c * tcov.syz;
880-
arrmat[ZZ] += tcov.szz;
881+
tcov.set(mCandTr[ord][i], TrackCovI::XRegNone);
882+
addRotatedTrackInfo(arrmat, mTrAux[i], tcov);
881883
}
882884
const double maxDiag = o2::gpu::GPUCommonMath::Max(o2::gpu::GPUCommonMath::Max(info(0, 0), info(1, 1)), info(2, 2));
883885
const double det2 = info(0, 0) * info(1, 1) - info(1, 0) * info(1, 0);
884886
const double det3 = info(0, 0) * (info(1, 1) * info(2, 2) - info(2, 1) * info(2, 1)) -
885887
info(1, 0) * (info(1, 0) * info(2, 2) - info(2, 1) * info(2, 0)) +
886888
info(2, 0) * (info(1, 0) * info(2, 1) - info(1, 1) * info(2, 0));
887889
constexpr double MinRelDet = 1.e-12;
888-
constexpr double InflateRelDet = 1.e-6;
889-
constexpr double MaxInflation = 1.e4;
890890
const bool isWellConditionedInfo = maxDiag > 0. && info(0, 0) > 0. && det2 > 0. && det3 > MinRelDet * maxDiag * maxDiag * maxDiag;
891891
if (isWellConditionedInfo) {
892892
auto cov = info;
893893
if (cov.Invert() && cov(0, 0) > 0. && cov(1, 1) > 0. && cov(2, 2) > 0.) {
894+
// TODO: for the collinear mode the covariance along the (badly defined) common direction
895+
// may need an extra inflation, calcCollinearInflation() provides a candidate scaling.
896+
// Kept disabled until validated on data.
894897
// if (mIsCollinear) {
895898
// cov *= calcCollinearInflation(cand);
896899
// }
897900
return cov;
898901
}
899902
}
900903
if (mLoggerBadPCACov.needToLog()) {
901-
printf("fitter %d: error (%ld muted): override ill-conditioned PCACovMatrix by dummy matrix", mFitterID, mLoggerBadPCACov.evCount);
904+
printf("fitter %d: error (%ld muted): override ill-conditioned PCACovMatrix by dummy matrix\n", mFitterID, mLoggerBadPCACov.evCount);
902905
}
903906
// Fall back on a deliberately loose vertex covariance. Returning a tight
904907
// identity covariance for a singular or ill-conditioned information matrix
@@ -966,23 +969,27 @@ GPUdi() double DCAFitterN<N, Args...>::calcChi2NoErr() const
966969
template <int N, typename... Args>
967970
GPUd() bool DCAFitterN<N, Args...>::correctTracks(const VecND& corrX)
968971
{
969-
// Propagate the actual candidate tracks to the updated X. Use the analytic
970-
// constant-Bz transport here: Newton corrections are small, but the track
971-
// state must stay synchronized with mTrPos for the next derivative update.
972+
// Propagate the actual candidate tracks to the updated X. Updating only mTrPos by a Taylor
973+
// expansion (as was done before) leaves mCandTr at the previous X, hence calcTrackDerivatives()
974+
// (which reads mCandTr) stays insensitive to the update and the slopes/curvatures remain frozen
975+
// at the seed for all Newton iterations.
976+
// The analytic constant-Bz transport is used on purpose (rather than propagate{Param}ToX with the
977+
// Propagator and material corrections): the Newton corrections are small, but the track state must
978+
// stay synchronized with mTrPos for the next derivative update. The final propagation to the PCA
979+
// (propagateTracksToVertex) refetches the original tracks and does use the full transport.
972980
for (int i = N; i--;) {
973-
/*
974-
// Updating only mTrPos by Taylor expansion leaves mCandTr at the previous X,
975-
// leaving calcTrackDerivatives() insensitive to the update. Use full fast propagation instead.
976-
const auto& trDer = mTrDer[mCurHyp][i];
977-
auto dx2h = 0.5 * corrX[i] * corrX[i];
978-
mTrPos[mCurHyp][i][0] -= corrX[i];
979-
mTrPos[mCurHyp][i][1] -= trDer.dydx * corrX[i] - dx2h * trDer.d2ydx2;
980-
mTrPos[mCurHyp][i][2] -= trDer.dzdx * corrX[i] - dx2h * trDer.d2zdx2;
981-
*/
982981
auto& trc = mCandTr[mCurHyp][i];
983982
const float x = static_cast<float>(mTrPos[mCurHyp][i][0] - corrX[i]);
984983
const bool propagated = mUseAbsDCA ? trc.propagateParamTo(x, mBz) : trc.propagateTo(x, mBz);
985-
if (!propagated) {
984+
if (!propagated) { // flag and log as done by propagate{Param}ToX
985+
mPropFailed[mCurHyp] = true;
986+
if (mLoggerBadProp.needToLog()) {
987+
#ifndef GPUCA_GPUCODE
988+
printf("fitter %d: error (%ld muted): Newton step propagation to %.4f failed for %s\n", mFitterID, mLoggerBadProp.evCount, x, trc.asString().c_str());
989+
#else
990+
printf("fitter %d: error (%ld muted): Newton step propagation to %.4f failed\n", mFitterID, mLoggerBadProp.evCount, x);
991+
#endif
992+
}
986993
return false;
987994
}
988995
setTrackPos(mTrPos[mCurHyp][i], trc);
@@ -1277,45 +1284,24 @@ GPUd() void DCAFitterN<N, Args...>::print() const
12771284
template <int N, typename... Args>
12781285
GPUd() o2::track::TrackParCov DCAFitterN<N, Args...>::createParentTrackParCov(int cand, bool sectorAlpha) const
12791286
{
1280-
std::array<float, 21> covV = {0.};
1287+
std::array<float, o2::track::kLabCovMatSize> covV = {0.};
12811288
std::array<float, 3> pvecV = {0.};
12821289
int q = 0;
12831290
for (int it = 0; it < N; it++) {
12841291
const auto& trc = getTrack(it, cand);
12851292
std::array<float, 3> pvecT = {0.};
1286-
const bool hasMomentum = trc.getPxPyPzGlo(pvecT);
1287-
1288-
// Propagate only the native momentum-parameter covariance
1289-
// (snp,tgl,q/pt) to the lab momentum covariance. The final constructor
1290-
// below rotates the summed lab covariance to the parent alpha frame.
1291-
if (hasMomentum) {
1292-
const double snp = trc.getSnp();
1293-
const double csp = trc.getCsp();
1294-
const double pt = trc.getPt();
1295-
const double alpha = trc.getAlpha();
1296-
double sna = 0., csa = 0.;
1297-
o2::math_utils::detail::sincos(alpha, sna, csa);
1298-
const double dPxdSnp = -pt * (snp * csa / csp + sna);
1299-
const double dPydSnp = pt * (csa - snp * sna / csp);
1300-
const double dPzdTgl = pt;
1301-
const double q2ptI = 1. / trc.getQ2Pt();
1302-
const double dPxdQ = -pvecT[0] * q2ptI;
1303-
const double dPydQ = -pvecT[1] * q2ptI;
1304-
const double dPzdQ = -pvecT[2] * q2ptI;
1305-
const double cSnpSnp = trc.getSigmaSnp2();
1306-
const double cTglSnp = trc.getSigmaTglSnp();
1307-
const double cTglTgl = trc.getSigmaTgl2();
1308-
const double cQSnp = trc.getSigma1PtSnp();
1309-
const double cQTgl = trc.getSigma1PtTgl();
1310-
const double cQQ = trc.getSigma1Pt2();
1311-
covV[9] += dPxdSnp * dPxdSnp * cSnpSnp + 2. * dPxdSnp * dPxdQ * cQSnp + dPxdQ * dPxdQ * cQQ;
1312-
covV[13] += dPydSnp * (dPxdSnp * cSnpSnp + dPxdQ * cQSnp) + dPydQ * (dPxdSnp * cQSnp + dPxdQ * cQQ);
1313-
covV[14] += dPydSnp * dPydSnp * cSnpSnp + 2. * dPydSnp * dPydQ * cQSnp + dPydQ * dPydQ * cQQ;
1314-
covV[18] += dPzdTgl * (dPxdSnp * cTglSnp + dPxdQ * cQTgl) + dPzdQ * (dPxdSnp * cQSnp + dPxdQ * cQQ);
1315-
covV[19] += dPzdTgl * (dPydSnp * cTglSnp + dPydQ * cQTgl) + dPzdQ * (dPydSnp * cQSnp + dPydQ * cQQ);
1316-
covV[20] += dPzdTgl * dPzdTgl * cTglTgl + 2. * dPzdTgl * dPzdQ * cQTgl + dPzdQ * dPzdQ * cQQ;
1293+
std::array<float, o2::track::kLabCovMatSize> covT = {0.};
1294+
trc.getPxPyPzGlo(pvecT);
1295+
// The momentum block of getCovXYZPxPyPzGlo is already J*C*J^T for the native O2 momentum
1296+
// parameters (snp,tgl,q/pt), with the track-frame alpha rotation folded into J, so there is
1297+
// no need to re-derive it here (and both methods share the same |q/pt|/|snp| validity guard,
1298+
// zeroing the covariance if it fails). The daughter momentum covariances are summed in the
1299+
// lab px,py,pz frame; the TrackParCov constructor below rotates the sum to the parent frame.
1300+
trc.getCovXYZPxPyPzGlo(covT);
1301+
constexpr int MomInd[6] = {9, 13, 14, 18, 19, 20}; // cov matrix elements for momentum component
1302+
for (int i = 0; i < 6; i++) {
1303+
covV[MomInd[i]] += covT[MomInd[i]];
13171304
}
1318-
13191305
for (int i = 0; i < 3; i++) {
13201306
pvecV[i] += pvecT[i];
13211307
}

0 commit comments

Comments
 (0)