BmnRoot 26.10.0-210-gc52d34b0
Software framework of the BM@N experiment
Loading...
Searching...
No Matches
BmnAlignHierarchy.h
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 BmnAlignHierarchy class header-only implementation
10 Hierarchical (multi-level) parametrization of the alignable detector
11*/
12
13#ifndef BMNALIGNHIERARCHY_H
14#define BMNALIGNHIERARCHY_H
15
16#include "BmnAlignDefines.h"
17#include "BmnDetectorModel.h"
18#include "BmnParamLayout.h"
19#include "fairlogger/Logger.h"
20
21#include <TMatrixD.h>
22#include <TString.h>
23#include <TVectorD.h>
24#include <string>
25#include <vector>
26
27/*
28 * Hierarchical parametrization
29 * ============================
30 *
31 * The flat parametrization assigns 6 rigid-body corrections to every alignable
32 * detector element (leaf). The hierarchical one adds *composite* nodes -
33 * subsystems, stations, half-stations - each carrying its own 6
34 * corrections, so that the effective correction of a leaf becomes, to first order
35 *
36 * a_leaf = a_leaf_own + SUM over ancestors J(leaf, ancestor) * a_ancestor
37 *
38 * The point of the exercise is presigmas: tight priors on leaf parameters
39 * together with loose priors on composite ones route chi2-ambiguous content
40 * into plausible large-scale motion of whole structures instead of a swarm of
41 * independent small element wiggles.
42 *
43 * Propagation Jacobian
44 * --------------------
45 * BmnMeasureModel parametrizes an element as V' = R (V - P) + P + d with the
46 * pivot P on the beam axis at the element's own z. Requiring a composite with
47 * pivot P_c to move its leaves rigidly and matching the two parametrizations
48 * gives,
49 *
50 * R_leaf = R_comp
51 * d_leaf = d_comp + (R_comp - I) * dP, dP = P_leaf - P_comp
52 *
53 * which, spelled out, is the 6x6 block stored in J:
54 *
55 * dX_leaf = dX_c + theta_c * dPz - psi_c * dPy
56 * dY_leaf = dY_c - phi_c * dPz + psi_c * dPx
57 * dZ_leaf = dZ_c + phi_c * dPy - theta_c * dPx
58 *
59 * With the natural pivot choice on the beam axis (X_c = Y_c = 0) this reduces to
60 *
61 * dX_leaf = dX_c + theta_c * dPz
62 * dY_leaf = dY_c - phi_c * dPz
63 * dZ_leaf = dZ_c
64 *
65 * exactly the shear / telescope-rotation weak modes, now carrying an explicit name
66 * and an explicit prior.
67 *
68 * Parameter layout
69 * ----------------
70 * The *full* layout: the parameters of every leaf, followed by those of every
71 * composite. The linear system is assembled and solved in it, and the
72 * constraints and presigmas config files address it.
73 *
74 * Keeping leaves first means the hierarchical basis extends the leaf basis
75 * rather than replacing it: T = [ I | J ], so with no composite nodes the
76 * hierarchical system is *identical* to the flat one.
77 *
78 * Note on exactness
79 * -----------------
80 * PromoteSystem() applies the basis change to the already reduced system produced
81 * by the Millepede dimension reduction. That is algebraically exact as long as J
82 * does not depend on the individual hit, which is why leaf pivots use a fixed
83 * nominal z per element rather than the per-hit z. For planar elements the
84 * residual effect is of order (hit z spread) x (rotation) and negligible;
85 * BmnAligner logs the observed z spread so the assumption stays checkable.
86 */
87
89{
90 public:
91 struct Node_t
92 {
93 std::string name; // human readable label used in reports
94 Int_t level{0}; // 0 for a root node, +1 per generation down
95 Int_t parent{-1}; // index in Nodes(), -1 for a root node
96 Int_t leaf{-1}; // detector element index for leaves, -1 for composites
97 Int_t composite{-1}; // order among composites for composites, -1 for leaves
98 SVect3 pivot{}; // rotation centre in the global frame
99 Bool_t pivotSet{kFALSE}; // kFALSE means "average the owned leaves"
100 };
101
102 BmnAlignHierarchy() = default;
103 virtual ~BmnAlignHierarchy() = default;
104
105 // --- construction: BeginBuild, then any number of AddComposite, then EndBuild ---
106 // leafZ holds the nominal z of every element (see the note on exactness above)
107 inline void BeginBuild(const BmnDetectorModel& detModel, const VecDouble_t& leafZ);
108 inline Bool_t AddComposite(const std::string& name,
109 const std::string& parent, // empty for a root node
110 const VecInt_t& leaves, // detector element indices owned directly
111 const SVect3* pivot // null to average the owned leaves
112 );
113 inline Bool_t EndBuild();
114 inline Bool_t BuildFlat(const BmnDetectorModel& detModel)
115 { // for special cases like BmnAlignerApply
116 BeginBuild(detModel, VecDouble_t(detModel.Alignables(), 0.0));
117 return EndBuild();
118 }
119
120 Bool_t Ready() const noexcept { return fReady; }
121 const BmnDetectorModel* DetModel() const noexcept { return fpDetModel; }
122 const BmnParamLayout& Layout() const noexcept { return fLayout; }
123
124 // dimensions of the full layout: <X>Count counts objects, <X>Params their 6 corrections each
125 Int_t LeafCount() const noexcept { return fLeavesTotal; }
126 Int_t CompositeCount() const noexcept { return fCompositesTotal; }
127 Int_t LeafParams() const noexcept { return fLeavesTotal * BMN_GLOBAL_PARAMS_PD; }
129 Int_t AllParams() const noexcept { return LeafParams() + CompositeParams(); }
130 Int_t CompositeParamBase() const noexcept { return LeafParams(); }
131
132 /*
133 * Transformation scheme. Hierarchical <==> effective values.
134 */
135
136 const std::vector<Node_t>& Nodes() const noexcept { return fNodes; }
137 const TMatrixD& J() const noexcept { return fJ; }
138
139 inline void ToLeaf(const TVectorD& hier, TVectorD& leaf) const;
140 inline void ToEffectiveCov(const TMatrixD& hier, TMatrixD& eff) const;
141 inline TMatrixD PromoteConstraints(const TMatrixD& constraints) const;
142 inline void PromoteSystem(const TVectorD& vB, const TMatrixD& mC, TVectorD& vB_h, TMatrixD& mC_h) const;
143
144 protected:
145 const BmnDetectorModel* fpDetModel{nullptr}; // set by BeginBuild()
148 BmnParamLayout fLayout; // derived view, built by EndBuild()
149 std::vector<Node_t> fNodes; // leaves first, composite nodes after them
150 VecInt_t fCompositeNode; // composite order -> node index
151 TMatrixD fJ; // LeafParams() x CompositeParams()
152
153 inline Int_t FindNode(const std::string& name) const;
154 inline void FillBlock(Int_t row, Int_t col, const SVect3& dP);
155};
156
157// === inline implementation ====================================================
158
159inline void BmnAlignHierarchy::BeginBuild(const BmnDetectorModel& detModel, const VecDouble_t& leafZ)
160{
161 fReady = kFALSE;
162 fpDetModel = &detModel;
164 fLeavesTotal = detModel.Alignables();
165 fNodes.clear();
166 fCompositeNode.clear();
167
168 // ---- leaf level: one node per alignable detector element ----
169 fNodes.reserve(fLeavesTotal);
170 for (Int_t i = 0; i < fLeavesTotal; i++) {
171 Node_t node;
172 node.name =
173 Form("det %2d (id %9d, %s)", i, detModel.EncodedID(i), detModel.DecodeID(detModel.EncodedID(i)).c_str());
174 node.leaf = i;
175 // the measurement model rotates an element about the beam axis at its own z
176 node.pivot = SVect3(0.0, 0.0, leafZ[i]);
177 node.pivotSet = kTRUE;
178 fNodes.push_back(node);
179 }
180}
181
182inline Int_t BmnAlignHierarchy::FindNode(const std::string& name) const
183{
184 for (std::size_t i = 0; i < fNodes.size(); i++)
185 if (fNodes[i].name == name)
186 return static_cast<Int_t>(i);
187 return -1;
188}
189
190inline Bool_t BmnAlignHierarchy::AddComposite(const std::string& name,
191 const std::string& parent,
192 const VecInt_t& leaves,
193 const SVect3* pivot)
194{
195 if (fNodes.empty()) {
196 LOG(error) << " <!> AddComposite: BeginBuild() was not called";
197 return kFALSE;
198 }
199 if (name.empty()) {
200 LOG(error) << " <!> AddComposite: node name must not be empty";
201 return kFALSE;
202 }
203 if (FindNode(name) >= 0) {
204 LOG(error) << " <!> AddComposite: duplicate node name '" << name << "'";
205 return kFALSE;
206 }
207 Int_t parentIdx{-1};
208 if (!parent.empty()) {
209 parentIdx = FindNode(parent);
210 if (parentIdx < 0) {
211 LOG(error) << " <!> AddComposite: node '" << name << "' refers to unknown parent '" << parent << "'";
212 return kFALSE;
213 }
214 if (fNodes[parentIdx].leaf >= 0) {
215 LOG(error) << " <!> AddComposite: node '" << name << "' cannot descend from leaf '" << parent << "'";
216 return kFALSE;
217 }
218 }
219
220 Node_t node;
221 node.name = name;
222 node.parent = parentIdx;
224 if (pivot) {
225 node.pivot = *pivot;
226 node.pivotSet = kTRUE;
227 }
228 const Int_t nodeIdx{static_cast<Int_t>(fNodes.size())};
229
230 // adopt the listed leaves - each may have one direct parent only
231 for (Int_t leafIdx : leaves) {
232 if (leafIdx < 0 || leafIdx >= fLeavesTotal) {
233 LOG(error) << " <!> AddComposite: node '" << name << "' lists element " << leafIdx << " out of range [0, "
234 << fLeavesTotal << ")";
235 return kFALSE;
236 }
237 if (fNodes[leafIdx].parent >= 0) {
238 LOG(error) << " <!> AddComposite: element " << leafIdx << " already belongs to '"
239 << fNodes[fNodes[leafIdx].parent].name << "', cannot also belong to '" << name << "'";
240 return kFALSE;
241 }
242 fNodes[leafIdx].parent = nodeIdx;
243 }
244
245 fNodes.push_back(node);
246 fCompositeNode.push_back(nodeIdx);
248 return kTRUE;
249}
250
252{
253 if (fNodes.empty()) {
254 LOG(error) << " <!> EndBuild: BeginBuild() was not called";
255 return kFALSE;
256 }
257
258 // ---- levels, and cycle detection along the way ----
259 for (Node_t& node : fNodes) {
260 Int_t level{0}, walk{node.parent};
261 while (walk >= 0) {
262 if (++level > static_cast<Int_t>(fNodes.size())) {
263 LOG(error) << " <!> EndBuild: parent cycle detected at node '" << node.name << "'";
264 return kFALSE;
265 }
266 walk = fNodes[walk].parent;
267 }
268 node.level = level;
269 }
270
271 // ---- default pivots: centre of gravity of the owned leaves ----
272 for (Int_t nodeIdx : fCompositeNode) {
273 Node_t& node{fNodes[nodeIdx]};
274 if (node.pivotSet)
275 continue;
276 SVect3 sum;
277 Int_t cnt{0};
278 for (Int_t l = 0; l < fLeavesTotal; l++) {
279 for (Int_t walk = fNodes[l].parent; walk >= 0; walk = fNodes[walk].parent)
280 if (walk == nodeIdx) {
281 sum += fNodes[l].pivot;
282 cnt++;
283 break;
284 }
285 }
286 if (!cnt) {
287 LOG(error) << " <!> EndBuild: composite '" << node.name << "' owns no detector element";
288 return kFALSE;
289 }
290 node.pivot = sum / static_cast<Double_t>(cnt);
291 node.pivotSet = kTRUE;
292 LOG_IF(warning, cnt == 1) << " ! composite '" << node.name
293 << "' owns a single element: its motion is"
294 " already fully described by that element's own parameters";
295 }
296
297 /*
298 * Every composite makes T = [ I | J ] rank deficient by its own 6 columns -
299 * that is the intended freedom. A composite owning leaves only through
300 * child nodes is different in kind: its parameters lie in the span of the
301 * children's, so J itself loses 6 columns. What that costs is the ability
302 * to pin the node with leaf priors, because the corresponding null vector
303 * of T is (-J c, c) with J c = 0, that is with a *zero leaf part* - no
304 * presigma placed on a leaf can see it. Such a node needs presigmas of its
305 * own, or the parent-versus-children split stays undetermined.
306 */
307 for (Int_t nodeIdx : fCompositeNode) {
308 Bool_t ownsDirect{kFALSE};
309 for (Int_t l = 0; l < fLeavesTotal && !ownsDirect; l++)
310 ownsDirect = fNodes[l].parent == nodeIdx;
311 LOG_IF(warning, !ownsDirect) << " ! composite '" << fNodes[nodeIdx].name
312 << "' owns every element"
313 " through child nodes: its parameters are a combination of theirs,"
314 " so the data cannot separate the two levels at all - the split"
315 " rests on the presigmas alone and converges slowly";
316 }
317
318 // ---- propagation Jacobian ----
319 if (fCompositesTotal) {
320 fJ.ResizeTo(LeafParams(), CompositeParams());
321 fJ.Zero();
322 for (Int_t l = 0; l < fLeavesTotal; l++)
323 for (Int_t walk = fNodes[l].parent; walk >= 0; walk = fNodes[walk].parent)
325 fNodes[l].pivot - fNodes[walk].pivot);
326 } else
327 fJ.ResizeTo(0, 0);
328
329 fReady = kTRUE;
330 fLayout.Rebuild(*this);
331 LOG(info) << " " << fNodes.size() << " hierarchy node(s): " << fLeavesTotal << " leaves, " << fCompositesTotal
332 << " composite(s)";
333 for (Int_t c = 0; c < fCompositesTotal; c++) {
334 const Node_t& node{fNodes[fCompositeNode[c]]};
335 LOG(info) << Form(" level %d : %-12s pivot (%.3f, %.3f, %.3f) cm", node.level, node.name.c_str(),
336 node.pivot(iX), node.pivot(iY), node.pivot(iZ));
337 }
338 return fReady;
339}
340
341inline void BmnAlignHierarchy::FillBlock(Int_t row, Int_t col, const SVect3& dP)
342{
343 // 6x6 propagation block, see the derivation in the header comment above
344 for (Int_t p = 0; p < BMN_GLOBAL_PARAMS_PD; p++)
345 fJ(row + p, col + p) = 1.0; // translations and angles carry over
346 fJ(row + idX, col + iTheta) = dP(iZ);
347 fJ(row + idX, col + iPsi) = -dP(iY);
348 fJ(row + idY, col + iPhi) = -dP(iZ);
349 fJ(row + idY, col + iPsi) = dP(iX);
350 fJ(row + idZ, col + iPhi) = dP(iY);
351 fJ(row + idZ, col + iTheta) = -dP(iX);
352}
353
354/*
355 * A constraint is written in the full layout, as coefficients on leaf and
356 * composite parameters, one row per constraint. Its row in the solved system is
357 *
358 * [ c_leaf ; J^T c_leaf + c_composite ]
359 *
360 * the J^T term being what keeps a coefficient placed on a leaf constraining
361 * the same physical quantity once composites carry part of the correction.
362 * For all rows at once that is [ C_leaf | C_leaf J + C_composite ].
363 */
365{
366 if (!fCompositesTotal || !constraints.GetNrows())
367 return constraints;
368
369 const Int_t nLeaf{LeafParams()}, lastRow{constraints.GetNrows() - 1};
370 TMatrixD cComp{constraints.GetSub(0, lastRow, nLeaf, AllParams() - 1)};
371 cComp += TMatrixD(constraints.GetSub(0, lastRow, 0, nLeaf - 1), TMatrixD::kMult, fJ);
372
373 TMatrixD rows{constraints};
374 rows.SetSub(0, nLeaf, cComp);
375 return rows;
376}
377
379 const TMatrixD& mC,
380 TVectorD& vB_h,
381 TMatrixD& mC_h) const
382{
383 const Int_t nLeaf{LeafParams()}, nHier{AllParams()};
384 vB_h.ResizeTo(nHier);
385 mC_h.ResizeTo(nHier, nHier);
386 vB_h.Zero();
387 mC_h.Zero();
388 // leading block: the leaf-basis system enters unchanged, since T = [ I | J ]
389 vB_h.SetSub(0, vB);
390 mC_h.SetSub(0, 0, mC);
391 if (!fCompositesTotal)
392 return;
393
394 // C' = T^T C T = [ C , C J ; (C J)^T , J^T C J ], b' = T^T b = [ b ; J^T b ]
395 TMatrixD mCJ{mC, TMatrixD::kMult, fJ}; // nLeaf x nComp
396 TMatrixD mJT{TMatrixD::kTransposed, fJ}; // nComp x nLeaf
397 mC_h.SetSub(0, nLeaf, mCJ);
398 mC_h.SetSub(nLeaf, 0, TMatrixD(TMatrixD::kTransposed, mCJ));
399 mC_h.SetSub(nLeaf, nLeaf, TMatrixD(mJT, TMatrixD::kMult, mCJ));
400 vB_h.SetSub(nLeaf, mJT * vB);
401}
402
403inline void BmnAlignHierarchy::ToLeaf(const TVectorD& hier, TVectorD& leaf) const
404{
405 const Int_t nLeaf{LeafParams()};
406 leaf.ResizeTo(nLeaf);
407 hier.GetSub(0, nLeaf - 1, leaf);
408 if (!fCompositesTotal)
409 return;
410 // a_leaf = a_leaf_own + J * a_composite
412 hier.GetSub(nLeaf, AllParams() - 1, comp);
413 leaf += fJ * comp;
414}
415
416inline void BmnAlignHierarchy::ToEffectiveCov(const TMatrixD& hier, TMatrixD& eff) const
417{
418 /*
419 * The results are quoted in a mixed basis: a leaf reports its *effective*
420 * correction a_leaf = a_leaf_own + J a_composite, a composite its own
421 * share. That is the linear map
422 *
423 * S = [ I J ] applied as Cov_eff = S Cov S^T
424 * [ 0 I ]
425 *
426 * giving, block by block,
427 *
428 * ll : Cov_ll + J Cov_cl + Cov_lc J^T + J Cov_cc J^T
429 * lc : Cov_lc + J Cov_cc cl : Cov_cl + Cov_cc J^T
430 * cc : Cov_cc (a composite is solved directly)
431 *
432 * The ll leading term alone would be the uncertainty of the element's own
433 * part; the rest is the share it inherits from its ancestors, which is what
434 * makes a tightly-primed leaf inside a loosely-primed composite come out
435 * with an honest error rather than the prior's.
436 */
437 eff.ResizeTo(hier);
438 eff = hier;
439 if (!fCompositesTotal)
440 return;
441
442 const Int_t nLeaf{LeafParams()}, last{AllParams() - 1};
443 // taken as separate blocks rather than transposed into one another: a
444 // numerically inverted matrix is only symmetric up to rounding
445 const TMatrixD covLC{hier.GetSub(0, nLeaf - 1, nLeaf, last)}, covCL{hier.GetSub(nLeaf, last, 0, nLeaf - 1)},
446 covCC{hier.GetSub(nLeaf, last, nLeaf, last)};
447 TMatrixD ll{hier.GetSub(0, nLeaf - 1, 0, nLeaf - 1)};
448 ll += TMatrixD(covLC, TMatrixD::kMultTranspose, fJ);
449 ll += TMatrixD(fJ, TMatrixD::kMult, covCL);
451 TMatrixD lc{covLC};
452 lc += TMatrixD(fJ, TMatrixD::kMult, covCC);
453 TMatrixD cl{covCL};
454 cl += TMatrixD(covCC, TMatrixD::kMultTranspose, fJ);
455
456 eff.SetSub(0, 0, ll);
457 eff.SetSub(0, nLeaf, lc);
458 eff.SetSub(nLeaf, 0, cl); // the cc block is already in place
459}
460
461#endif // BMNALIGNHIERARCHY_H
std::vector< Double_t > VecDouble_t
RM::SVector< Double_t, 3 > SVect3
@ idX
@ iPhi
@ idZ
@ iPsi
@ iTheta
@ idY
std::vector< Int_t > VecInt_t
@ iZ
@ iX
@ iY
#define BMN_GLOBAL_PARAMS_PD
int i
Definition P4_F32vec4.h:22
bool Bool_t
int Int_t
constexpr Bool_t kFALSE
double Double_t
constexpr Bool_t kTRUE
TMatrixT< Double_t > TMatrixD
char * Form(const char *fmt,...)
TVectorT< Double_t > TVectorD
Int_t LeafCount() const noexcept
const TMatrixD & J() const noexcept
Int_t FindNode(const std::string &name) const
Int_t LeafParams() const noexcept
Int_t CompositeCount() const noexcept
Bool_t BuildFlat(const BmnDetectorModel &detModel)
virtual ~BmnAlignHierarchy()=default
const std::vector< Node_t > & Nodes() const noexcept
Int_t AllParams() const noexcept
void ToEffectiveCov(const TMatrixD &hier, TMatrixD &eff) const
const BmnParamLayout & Layout() const noexcept
Int_t CompositeParams() const noexcept
Bool_t Ready() const noexcept
void PromoteSystem(const TVectorD &vB, const TMatrixD &mC, TVectorD &vB_h, TMatrixD &mC_h) const
BmnAlignHierarchy()=default
Int_t CompositeParamBase() const noexcept
std::vector< Node_t > fNodes
void BeginBuild(const BmnDetectorModel &detModel, const VecDouble_t &leafZ)
BmnParamLayout fLayout
void FillBlock(Int_t row, Int_t col, const SVect3 &dP)
const BmnDetectorModel * DetModel() const noexcept
TMatrixD PromoteConstraints(const TMatrixD &constraints) const
const BmnDetectorModel * fpDetModel
Bool_t AddComposite(const std::string &name, const std::string &parent, const VecInt_t &leaves, const SVect3 *pivot)
void ToLeaf(const TVectorD &hier, TVectorD &leaf) const
Int_t EncodedID(Int_t idx) const
virtual std::string DecodeID(Int_t id) const =0
virtual Int_t Alignables() const =0
TMatrixT< Element > GetSub(Int_t row_lwb, Int_t row_upb, Int_t col_lwb, Int_t col_upb, Option_t *option="S") const
TMatrixTBase< Element > & ResizeTo(const TMatrixT< Element > &m)
TMatrixTBase< Element > & SetSub(Int_t row_lwb, Int_t col_lwb, const TMatrixTBase< Element > &source) override
TVectorT< Element > & Zero()
TVectorT< Element > & SetSub(Int_t row_lwb, const TVectorT< Element > &source)
TVectorT< Element > & ResizeTo(const TVectorT< Element > &v)
TVectorT< Element > GetSub(Int_t row_lwb, Int_t row_upb, Option_t *option="S") const