BmnRoot 26.10.0-210-gc52d34b0
Software framework of the BM@N experiment
Loading...
Searching...
No Matches
BmnAlignPriors.cxx
Go to the documentation of this file.
1/*
2 BM@N alignment routine
3 BM@N experiment at NICA complex, JINR, 2025
4
5 Department: Math & Soft Group of HEP lab
6 Author: Claude (Opus 5)
7 Supervisor: Igor Polev, polev@jinr.int
8
9 BmnAlignPriors class implementation
10*/
11
12#include "BmnAlignPriors.h"
13
14#include "TDecompSVD.h"
15#include "TString.h"
16#include "fairlogger/Logger.h"
17
18#include <algorithm>
19#include <cmath>
20#include <utility>
21#include <vector>
22
23namespace
24{
25// relative to the norm of a constraint row: below it the row adds nothing
26constexpr Double_t ROW_RANK_TOL{1.0E-9};
27// cm or rad per unit row norm: a dropped row asking for more contradicts
28constexpr Double_t CONTRADICTION_TOL{1.0E-9};
29} // namespace
30
31/*
32 * Fold the prior knowledge (presigmas) into the reduced system of normal
33 * equations C' dA = b' produced by the Millepede dimension reduction.
34 *
35 * A presigma is a pseudo-measurement of a single global parameter:
36 *
37 * "parameter k equals reference_k, with uncertainty sigma_k"
38 *
39 * Its residual is (reference_k - A_k), its derivative with respect to A_k is
40 * exactly 1 (and zero for every other global and local parameter), and its
41 * weight is 1/sigma_k^2. A measurement with residual r, weight w and
42 * derivative row g contributes C += w*g*g^T, b += w*g*r - and since g is
43 * the unit vector e_k, this collapses onto a single diagonal element and a
44 * single RHS entry.
45 *
46 * Two details are essential here:
47 *
48 * 1. Each iteration solves for the step dA while A accumulates, so the
49 * residual must reference the *accumulated* correction of the previous
50 * iteration - exactly as the constraint RHS does in IterateAlignment().
51 * With A = A_acc + dA the residual becomes (reference - A_acc).
52 * CAUTION: A_acc must be stated in the basis the system is solved in. For
53 * an element that is its own share, while the accumulated solution holds
54 * its effective correction a_acc + J P_acc. Measuring a leaf prior against
55 * the effective value makes the composites drift: the data cannot see the
56 * level split, so every iteration moves P by about the effective value.
57 *
58 * 2. The weight is stated in the same units as the hit weights of
59 * ProcessTracks, which are plain 1/sigma^2. A presigma of sigma therefore
60 * acts precisely like one extra hit measuring that parameter with
61 * resolution sigma.
62 *
63 * A single pseudo-measurement is weak against millions of real hits - and
64 * that is the intent: the prior is negligible for well determined parameters
65 * and dominates only where the data says (almost) nothing, i.e. along the
66 * weak / chi2-invariant modes it is meant to lift.
67 */
68void BmnAlignPriors::Apply(TVectorD& vB, TMatrixD& mC, const BmnAlignHierarchy& hierarchy, const TVectorD* accA) const
69{
70 if (!UsePresigmas())
71 return;
72
73 const TVectorD zero(hierarchy.AllParams());
74 const TVectorD& acc{accA ? *accA : zero};
75
76 // the ancestors' part of each element's accumulated correction, J P_acc
77 TVectorD vJP(hierarchy.LeafParams());
78 if (hierarchy.CompositeParams())
79 vJP = hierarchy.J() * acc.GetSub(hierarchy.CompositeParamBase(), hierarchy.AllParams() - 1);
80
81 for (const ParamRef& p : hierarchy.Layout().All()) {
82 const Double_t s{SigmaAt(p.full)};
83 if (s <= 0.0)
84 continue;
85 const Double_t w{1.0 / (s * s)}, own{p.IsComposite() ? acc(p.full) : acc(p.full) - vJP[p.full]};
86 mC(p.full, p.full) += w; // Hessian : + 1/sigma^2
87 vB[p.full] += w * (ref[p.full] - own); // gradient: + (ref - acc_own)/sigma^2
88 }
89}
90
92{
93 /*
94 * Constraint rows are kept only if they add something to the rows before
95 * them, tested by modified Gram-Schmidt on the promoted rows. A repeated
96 * row makes the bordered matrix singular, and a config may repeat itself.
97 * A row dropped so must ask for the value the kept rows already give; if
98 * it does not, the constraints contradict each other.
99 *
100 * The test does not depend on the iteration. The promotion is c -> c S
101 * with S = [ I J ; 0 I ] invertible, so a promoted row is a combination
102 * of other promoted rows exactly when the original row is the same
103 * combination of the original rows. The right-hand side the aligner uses,
104 * rhs - C A_acc, then reduces to that combination of the rhs from the file:
105 * the A_acc terms cancel.
106 */
107 const TMatrixD rows{hierarchy.PromoteConstraints(constraints)};
108 std::vector<TVectorD> basis; // orthonormal span of the rows kept so far
109 VecDouble_t basisRhs; // their right-hand sides, transformed alike
110 Int_t contradictions{0};
111 fKept.clear();
112 for (Int_t j = 0; j < ConstCount(); j++) {
113 const TVectorD row{TMatrixDRow_const(rows, j)};
114 TVectorD res{row};
115 Double_t resRhs{rhs[j]};
116 for (std::size_t m = 0; m < basis.size(); m++) {
117 const Double_t d{Dot(basis[m], res)};
118 Add(res, -d, basis[m]);
119 resRhs -= d * basisRhs[m];
120 }
121 const Double_t rowNorm{std::sqrt(row.Norm2Sqr())}, resNorm{std::sqrt(res.Norm2Sqr())};
122 if (resNorm > ROW_RANK_TOL * rowNorm) {
123 basis.push_back((1.0 / resNorm) * res);
124 basisRhs.push_back(resRhs / resNorm);
125 fKept.push_back(j);
126 continue;
127 }
128 const Double_t scale{rowNorm > 0.0 ? rowNorm : 1.0}; // an all-zero row asks 0 = rhs
129 if (std::fabs(resRhs) <= CONTRADICTION_TOL * scale)
130 continue;
131 LOG(error) << Form(" <!> BmnAlignPriors::Check: constraint row #%d asks for a value %.2e off what the"
132 " constraints before it give (or has an all-zero LHS with a non-zero RHS)",
133 j + 1, resRhs / scale);
134 contradictions++;
135 }
136 if (contradictions) {
137 LOG(error) << " <!> BmnAlignPriors::Check: " << contradictions << " contradicting constraint(s)";
138 return kFALSE;
139 }
140 fKeptRows.ResizeTo(KeptCount(), fAllParams);
141 for (Int_t i = 0; i < KeptCount(); i++)
143 LOG_IF(info, ConstCount()) << " " << KeptCount() << " constraint(s) kept, " << ConstCount() - KeptCount()
144 << " dropped as empty or repeated";
145
146 /*
147 * Composite parameters are redundant by construction: T = [ I | J ] has a
148 * null space { (-J c, c) }, one dimension per composite parameter, and the
149 * data says exactly nothing along it. Something else has to determine
150 * those directions, and only three things can:
151 *
152 * - a prior on leaf parameter l sees the direction as (J c)_l
153 * - a prior on composite parameter a sees it as c_a
154 * - a constraint sees it as c_direct^T c
155 *
156 * The constraint case is the one worth spelling out: its row in the
157 * hierarchical basis is [ c_leaf ; J^T c_leaf + c_direct ], which on
158 * (-J c, c) evaluates to c_direct^T c - the leaf part cancels exactly. So
159 * a constraint written on leaf parameters is *blind* to the composite
160 * split, however many of them there are, because it constrains the
161 * effective corrections A = T P and T v = 0 means A does not move. Only
162 * coefficients placed directly on a composite (NodeName) count here.
163 *
164 * Stacking those rows, the hierarchy is determined if they have full
165 * column rank. Only the presence of a prior matters, not how tight it is:
166 * any finite sigma lifts the exact degeneracy.
167 */
168 const Int_t nComp{hierarchy.CompositeParams()};
169 if (!nComp)
170 return kTRUE; // flat parametrization leaves nothing extra undetermined
171
172 const Int_t nFull{hierarchy.AllParams()};
173 // sigma stays empty when no presigmas file was given, hence SigmaAt()
174 TMatrixD probe(nFull + ConstCount(), nComp);
175 probe.Zero();
176 for (const ParamRef& p : hierarchy.Layout().All()) {
177 if (SigmaAt(p.full) <= 0.0)
178 continue;
179 if (p.IsComposite())
180 probe(p.full, p.comp) = 1.0; // prior placed on the node itself
181 else // leaf prior, seen through J
182 TMatrixDRow(probe, p.full) = TMatrixDRow_const(hierarchy.J(), p.full);
183 }
184 if (ConstCount())
185 probe.SetSub(nFull, 0, constraints.GetSub(0, ConstCount() - 1, hierarchy.CompositeParamBase(), nFull - 1));
186
187 // An all-zero probe is rank 0 by inspection, and SVD does not survive it.
188 // Worth its own message: it means nothing at all was configured, rather
189 // than what was configured being insufficient.
190 if (!probe.NonZeros()) {
191 LOG(error) << " <!> none of the " << nComp
192 << " composite parameter(s) is"
193 " determined: neither a presigma nor a composite-addressed"
194 " constraint was given, so the split between the levels rests"
195 " on nothing at all";
196 return kFALSE;
197 }
198 TDecompSVD svd(probe);
199 if (!svd.Decompose()) {
200 LOG(error) << " <!> BmnAlignPriors::Check: SVD failed, solvability not verified";
201 return kFALSE;
202 }
203 const TVectorD& sig{svd.GetSig()}; // TDecompSVD returns them in decreasing order
204 constexpr static const Double_t RANK_TOL{1.0E-9}; // relative to the largest singular value
205 Int_t rank{0};
206 while (rank < sig.GetNrows() && sig[rank] > sig[0] * RANK_TOL)
207 rank++;
208
209 if (rank == nComp) {
210 LOG(info) << " hierarchy check: all " << nComp
211 << " composite parameter(s) determined by priors and constraints";
212 return kTRUE;
213 }
214
215 // Report the undetermined directions themselves rather than the parameters
216 // lacking a direct prior: leaf priors pin composite parameters indirectly
217 // through J, so the two lists differ and only the former is actionable.
218 // Singular values come back in decreasing order, so the trailing right
219 // singular vectors span the null space.
220 const TMatrixD& V{svd.GetV()};
221 LOG(error) << " <!> " << nComp - rank << " of " << nComp
222 << " composite parameter direction(s) left undetermined -"
223 " the fit would be singular along them:";
224 for (Int_t d = rank; d < nComp; d++) {
225 std::vector<std::pair<Double_t, Int_t>> weight;
226 weight.reserve(nComp);
227 for (Int_t a = 0; a < nComp; a++)
228 weight.emplace_back(std::fabs(V(a, d)), a);
229 std::sort(weight.rbegin(), weight.rend());
230 TString descr;
231 for (Int_t t = 0; t < nComp && weight[t].first > 0.1; t++)
232 descr += Form("%s%+.2f x [%s]", descr.IsNull() ? "" : " ", V(weight[t].second, d),
233 hierarchy.Layout().Composites()[weight[t].second].Label().Data());
234 LOG(error) << " " << descr;
235 }
236 LOG(error) << " give the composites involved a presigma (NodeName entry) or a"
237 " NodeName constraint - leaf-addressed constraints cannot fix a"
238 " composite split, and a leaf presigma only reaches the combinations"
239 " that move its own element";
240 return kFALSE;
241}
std::vector< Double_t > VecDouble_t
int i
Definition P4_F32vec4.h:22
__m128 m
Definition P4_F32vec4.h:27
bool Bool_t
int Int_t
constexpr Bool_t kFALSE
double Double_t
constexpr Bool_t kTRUE
Double_t Dot(const TGLVector3 &v1, const TGLVector3 &v2)
TMatrixTRow< Double_t > TMatrixDRow
TMatrixTRow_const< Double_t > TMatrixDRow_const
TMatrixT< Double_t > TMatrixD
TMatrixTSparse< Element > & Add(TMatrixTSparse< Element > &target, Element scalar, const TMatrixTSparse< Element > &source)
char * Form(const char *fmt,...)
TVectorT< Double_t > TVectorD
const TMatrixD & J() const noexcept
Int_t LeafParams() const noexcept
Int_t AllParams() const noexcept
const BmnParamLayout & Layout() const noexcept
Int_t CompositeParams() const noexcept
Int_t CompositeParamBase() const noexcept
TMatrixD PromoteConstraints(const TMatrixD &constraints) const
Double_t SigmaAt(Int_t full) const noexcept
VecDouble_t ref
void Apply(TVectorD &vB, TMatrixD &mC, const BmnAlignHierarchy &hierarchy, const TVectorD *accA) const
TMatrixD constraints
Bool_t Check(const BmnAlignHierarchy &hierarchy)
Bool_t UsePresigmas() const noexcept
Int_t KeptCount() const noexcept
Int_t ConstCount() const noexcept
const Range_t & All() const noexcept
const Range_t & Composites() const noexcept
const TVectorD & GetSig()
const TMatrixD & GetV()
Bool_t Decompose() override
TMatrixTBase< Element > & SetSub(Int_t row_lwb, Int_t col_lwb, const TMatrixTBase< Element > &source) override
Bool_t IsNull() const
Int_t GetNrows() const
Element Norm2Sqr() const
TVectorT< Element > GetSub(Int_t row_lwb, Int_t row_upb, Option_t *option="S") const