Skip to content

Commit be98361

Browse files
committed
Add regularization for degenerate matrices
1 parent 4ce0813 commit be98361

2 files changed

Lines changed: 198 additions & 14 deletions

File tree

Common/DCAFitter/DCAFitterN_derivation.md

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,112 @@ TrackParCov(xyz, pxpypz, cov, charge, sectorAlpha)
431431
then transforms this lab covariance to the selected parent track frame,
432432
including the parent `alpha` convention.
433433
434-
## 10. Code changes summarized
434+
## 10. Regularization of singular or weakly constrained covariance matrices
435+
436+
### Single-track `TrackCovI`
437+
438+
The matrix
439+
440+
```text
441+
I = H^T W H
442+
```
443+
444+
has rank at most two for one track, because one track measures only the two
445+
coordinates transverse to its own trajectory. The null direction is the local
446+
track tangent
447+
448+
```text
449+
t = (1, y_prime, z_prime)^T .
450+
```
451+
452+
Indeed
453+
454+
```text
455+
H t = 0, I t = 0 .
456+
```
457+
458+
Therefore a single-track `TrackCovI` is positive semidefinite, not positive
459+
definite. Inverting this 3D matrix by itself is not meaningful; the apparent
460+
negative diagonal elements seen in such an inverse are numerical symptoms of
461+
trying to invert a rank-deficient information matrix. The physically meaningful
462+
inverse at single-track level is only the original 2 by 2 `(Y,Z)` covariance.
463+
464+
For the multi-track vertex fit, different track directions usually make
465+
466+
```text
467+
A = sum_i R_i I_i R_i^T
468+
```
469+
470+
full rank. However, nearly parallel or otherwise weak geometries can still make
471+
the longitudinal eigenvalue very small. To avoid an exactly singular
472+
single-track contribution while preserving the measured `(Y,Z)` block, the code
473+
may add a weak positive longitudinal prior only to `I_XX`:
474+
475+
```text
476+
I_XX -> I_XX + 1/sigma_X,prior^2 .
477+
```
478+
479+
In code this is implemented as
480+
481+
```cpp
482+
constexpr float XRegErrFactor = 10.f;
483+
const float sigmaX2 = C_YY * XRegErrFactor;
484+
sxx += 1.f / sigmaX2;
485+
```
486+
487+
This is different from multiplying `I_XX` by a number below one. A reduction of
488+
`I_XX` can make the matrix indefinite, while adding a positive diagonal term
489+
keeps the information matrix positive definite in the tangent direction and does
490+
not alter `I_YY`, `I_YZ`, or `I_ZZ`.
491+
492+
The regularization should remain weak. It is a numerical stabilizer for badly
493+
conditioned geometries, not an additional detector measurement of the local
494+
track `X` coordinate.
495+
496+
### `calcPCACovMatrix()`
497+
498+
The vertex covariance is
499+
500+
```text
501+
C_V = A^-1, A = sum_i R_i I_i R_i^T .
502+
```
503+
504+
If `A` is singular or ill-conditioned, returning a small fallback covariance
505+
would incorrectly shrink the uncertainty along the weakly constrained direction.
506+
The conservative behavior is:
507+
508+
1. Check that `A` is compatible with a positive-definite information matrix.
509+
2. Reject cases where the determinant is too small compared with the diagonal
510+
scale.
511+
3. Invert only when these checks pass.
512+
4. If inversion fails, or if the inverted covariance has non-positive diagonal
513+
elements, return a deliberately loose fallback covariance.
514+
515+
The code uses the leading-minor checks
516+
517+
```text
518+
A_XX > 0,
519+
det(A_XY block) > 0,
520+
det(A) > epsilon max(diag(A))^3
521+
```
522+
523+
with
524+
525+
```text
526+
epsilon = 1e-12 .
527+
```
528+
529+
On failure, it returns a diagonal covariance with
530+
531+
```text
532+
sigma^2 = 1e6 cm^2 .
533+
```
534+
535+
This corresponds to a 10 m uncertainty in each coordinate. It is intentionally
536+
loose: the fallback marks the parent vertex as poorly constrained rather than
537+
creating an artificially precise parent covariance.
538+
539+
## 11. Code changes summarized
435540

436541
1. `TrackCovI` now stores a full symmetric local 3D information matrix.
437542
2. The dummy `XerrFactor` approximation was removed.

Common/DCAFitter/include/DCAFitter/DCAFitterN.h

Lines changed: 92 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ struct TrackCovI {
5959
sxy = -(syy * dydx + syz * dzdx);
6060
sxz = -(syz * dydx + szz * dzdx);
6161
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;
6266
return res;
6367
}
6468
};
@@ -220,6 +224,7 @@ class DCAFitterN
220224
///< recalculate PCA as a cov-matrix weighted mean, even if absDCA method was used
221225
GPUd() bool recalculatePCAWithErrors(int cand = 0);
222226

227+
GPUd() double calcCollinearInflation(int cand) const;
223228
GPUd() MatSym3D calcPCACovMatrix(int cand = 0) const;
224229

225230
std::array<float, 6> calcPCACovMatrixFlat(int cand = 0) const
@@ -385,9 +390,10 @@ class DCAFitterN
385390
std::array<int, MAXHYP> mNIters; // number of iterations for each seed
386391
std::array<bool, MAXHYP> mTrPropDone{}; // Flag that the tracks are fully propagated to PCA
387392
std::array<bool, MAXHYP> mPropFailed{}; // Flag that some propagation failed for this PCA candidate
388-
LogLogThrottler mLoggerBadCov{};
389-
LogLogThrottler mLoggerBadInv{};
390-
LogLogThrottler mLoggerBadProp{};
393+
mutable LogLogThrottler mLoggerBadCov{};
394+
mutable LogLogThrottler mLoggerBadInv{};
395+
mutable LogLogThrottler mLoggerBadProp{};
396+
mutable LogLogThrottler mLoggerBadPCACov{};
391397
MatSym3D mWeightInv; // inverse weight of single track, [sum{M^T E M}]^-1 in EQ.T
392398
std::array<int, MAXHYP> mOrder{0};
393399
int mCurHyp = 0;
@@ -800,6 +806,50 @@ GPUd() void DCAFitterN<N, Args...>::calcPCANoErr()
800806
pca[2] *= NInv;
801807
}
802808

809+
//___________________________________________________________________
810+
template <int N, typename... Args>
811+
GPUd() double DCAFitterN<N, Args...>::calcCollinearInflation(int cand) const
812+
{
813+
std::array<std::array<double, 3>, N> u{};
814+
int nu = 0;
815+
816+
for (int i = 0; i < N; ++i) {
817+
std::array<float, 3> p{};
818+
if (!getTrack(i, cand).getPxPyPzGlo(p)) {
819+
continue;
820+
}
821+
const double p2 = p[0] * p[0] + p[1] * p[1] + p[2] * p[2];
822+
if (p2 <= 0.) {
823+
continue;
824+
}
825+
const double pI = 1. / std::sqrt(p2);
826+
u[nu++] = {p[0] * pI, p[1] * pI, p[2] * pI};
827+
}
828+
829+
if (nu < 2) {
830+
return 1.;
831+
}
832+
833+
double sin2Mean = 0.;
834+
int npairs = 0;
835+
for (int i = 0; i < nu; ++i) {
836+
for (int j = i + 1; j < nu; ++j) {
837+
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);
840+
++npairs;
841+
}
842+
}
843+
sin2Mean /= npairs;
844+
845+
constexpr double Sin2Ref = 1.e-5;
846+
constexpr double MaxInflation = 1.e4;
847+
if (sin2Mean <= 0.) {
848+
return MaxInflation;
849+
}
850+
return sin2Mean < Sin2Ref ? std::min(MaxInflation, Sin2Ref / sin2Mean) : 1.;
851+
}
852+
803853
//___________________________________________________________________
804854
template <int N, typename... Args>
805855
GPUd() o2::math_utils::SMatrix<double, 3, 3, o2::math_utils::MatRepSym<double, 3>> DCAFitterN<N, Args...>::calcPCACovMatrix(int cand) const
@@ -829,14 +879,34 @@ GPUd() o2::math_utils::SMatrix<double, 3, 3, o2::math_utils::MatRepSym<double, 3
829879
arrmat[YZ] += taux.s * tcov.sxz + taux.c * tcov.syz;
830880
arrmat[ZZ] += tcov.szz;
831881
}
832-
if (info.Invert()) {
833-
return info;
882+
const double maxDiag = o2::gpu::GPUCommonMath::Max(o2::gpu::GPUCommonMath::Max(info(0, 0), info(1, 1)), info(2, 2));
883+
const double det2 = info(0, 0) * info(1, 1) - info(1, 0) * info(1, 0);
884+
const double det3 = info(0, 0) * (info(1, 1) * info(2, 2) - info(2, 1) * info(2, 1)) -
885+
info(1, 0) * (info(1, 0) * info(2, 2) - info(2, 1) * info(2, 0)) +
886+
info(2, 0) * (info(1, 0) * info(2, 1) - info(1, 1) * info(2, 0));
887+
constexpr double MinRelDet = 1.e-12;
888+
constexpr double InflateRelDet = 1.e-6;
889+
constexpr double MaxInflation = 1.e4;
890+
const bool isWellConditionedInfo = maxDiag > 0. && info(0, 0) > 0. && det2 > 0. && det3 > MinRelDet * maxDiag * maxDiag * maxDiag;
891+
if (isWellConditionedInfo) {
892+
auto cov = info;
893+
if (cov.Invert() && cov(0, 0) > 0. && cov(1, 1) > 0. && cov(2, 2) > 0.) {
894+
// if (mIsCollinear) {
895+
// cov *= calcCollinearInflation(cand);
896+
// }
897+
return cov;
898+
}
899+
}
900+
if (mLoggerBadPCACov.needToLog()) {
901+
printf("fitter %d: error (%ld muted): override ill-conditioned PCACovMatrix by dummy matrix", mFitterID, mLoggerBadPCACov.evCount);
834902
}
835-
// Fall back on identity diagonal matrix with 1 cm error.
903+
// Fall back on a deliberately loose vertex covariance. Returning a tight
904+
// identity covariance for a singular or ill-conditioned information matrix
905+
// would shrink the uncertainty in the weakly constrained direction.
836906
memset(arrmat, 0, sizeof(info));
837-
info(0, 0) = 1.;
838-
info(1, 1) = 1.;
839-
info(2, 2) = 1.;
907+
info(0, 0) = 4.;
908+
info(1, 1) = 4.;
909+
info(2, 2) = 4.;
840910
return info;
841911
}
842912

@@ -900,6 +970,15 @@ GPUd() bool DCAFitterN<N, Args...>::correctTracks(const VecND& corrX)
900970
// constant-Bz transport here: Newton corrections are small, but the track
901971
// state must stay synchronized with mTrPos for the next derivative update.
902972
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+
*/
903982
auto& trc = mCandTr[mCurHyp][i];
904983
const float x = static_cast<float>(mTrPos[mCurHyp][i][0] - corrX[i]);
905984
const bool propagated = mUseAbsDCA ? trc.propagateParamTo(x, mBz) : trc.propagateTo(x, mBz);
@@ -1291,9 +1370,9 @@ GPUdi() bool DCAFitterN<N, Args...>::propagateParamToX(o2::track::TrackPar& t, f
12911370
mPropFailed[mCurHyp] = true;
12921371
if (mLoggerBadProp.needToLog()) {
12931372
#ifndef GPUCA_GPUCODE
1294-
printf("fitter %d: error (%ld muted): propagation failed for %s\n", mFitterID, mLoggerBadProp.evCount, t.asString().c_str());
1373+
printf("fitter %d: error (%ld muted): propagation to %.4f failed for %s\n", mFitterID, mLoggerBadProp.evCount, x, t.asString().c_str());
12951374
#else
1296-
printf("fitter %d: error (%ld muted): propagation failed\n", mFitterID, mLoggerBadProp.evCount);
1375+
printf("fitter %d: error (%ld muted): propagation to %.4f failed\n", mFitterID, mLoggerBadProp.evCount, x);
12971376
#endif
12981377
}
12991378
}
@@ -1317,9 +1396,9 @@ GPUdi() bool DCAFitterN<N, Args...>::propagateToX(o2::track::TrackParCov& t, flo
13171396
mPropFailed[mCurHyp] = true;
13181397
if (mLoggerBadProp.needToLog()) {
13191398
#ifndef GPUCA_GPUCODE
1320-
printf("fitter %d: error (%ld muted): propagation failed for %s\n", mFitterID, mLoggerBadProp.evCount, t.asString().c_str());
1399+
printf("fitter %d: error (%ld muted): propagation to %.4f failed for %s\n", mFitterID, mLoggerBadProp.evCount, x, t.asString().c_str());
13211400
#else
1322-
printf("fitter %d: error (%ld muted): propagation failed\n", mFitterID, mLoggerBadProp.evCount);
1401+
printf("fitter %d: error (%ld muted): propagation to %.4f failed\n", mFitterID, mLoggerBadProp.evCount, x);
13231402
#endif
13241403
}
13251404
}

0 commit comments

Comments
 (0)