BmnRoot 26.10.0-210-gc52d34b0
Software framework of the BM@N experiment
Loading...
Searching...
No Matches
BmnAlignConfig.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 BmnAlignConfig class implementation
10*/
11
12#include "BmnAlignConfig.h"
13
14#include "fairlogger/Logger.h"
15
16#include <algorithm>
17#include <cmath>
18#include <fstream>
19#include <iomanip>
20#include <nlohmann/json.hpp>
21#include <regex>
22#include <string>
23
24namespace
25{
26// A constraint RHS this small is meant as zero; see the comment at the use site.
27constexpr Double_t SOFT_ZERO{1.0E-6};
28
29// objects a config entry addresses; they point into the layout of the
30// hierarchy being loaded, which does not change while a file is read
31typedef std::vector<const NodeRef*> NodeList_t;
32
33/*
34 * Locators shared by the constraints and the presigmas files. Each returns
35 * an empty string on success, or what was wrong with the value, which the
36 * caller logs together with the number of the entry.
37 */
38
39// "ParamName": one name or an array of names -> parameter indices
40std::string ParamList(const nlohmann::json& js, VecInt_t& params)
41{
42 params.clear();
43 // CAUTION: no braces here - "json list {x}" makes the array [x], not a copy of x
44 const nlohmann::json list = js.is_array() ? js : nlohmann::json::array({js});
45 for (const nlohmann::json& name : list) {
46 const Int_t p{name.is_string() ? BmnParamLayout::ParamIndex(name.get<std::string>()) : -1};
47 if (p < 0)
48 return "unknown ParamName " + name.dump();
49 params.push_back(p);
50 }
51 return params.empty() ? "empty ParamName array" : "";
52}
53
54// "Over": a set of detector elements, never composites
55// Elements - all of them, the target included
56// Tracker - all but the target (all of them in a model without one)
57// Target - the target only (nothing in a model without one)
58std::string LeafScope(const nlohmann::json& js, const BmnAlignHierarchy& hierarchy, NodeList_t& nodes)
59{
60 nodes.clear();
61 const std::string over{js.is_string() ? js.get<std::string>() : js.dump()};
62 if (over != "Elements" && over != "Tracker" && over != "Target")
63 return "unknown Over value '" + over + "', expected Elements / Tracker / Target";
64 const BmnDetectorModel* model{hierarchy.DetModel()};
65 const Int_t target{model->TargetID() < 0 ? -1 : model->Idx(model->TargetID())};
66 for (const NodeRef& node : hierarchy.Layout().ByNode()) {
67 if (node.IsComposite())
68 continue;
69 const Bool_t isTarget{node.leaf == target};
70 if (over == "Elements" || (over == "Tracker" && !isTarget) || (over == "Target" && isTarget))
71 nodes.push_back(&node);
72 }
73 return "";
74}
75
76// "NodeMatch": the composites whose whole name matches a regular expression
77std::string MatchComposites(const nlohmann::json& js, const BmnAlignHierarchy& hierarchy, NodeList_t& nodes)
78{
79 nodes.clear();
80 if (!js.is_string())
81 return "NodeMatch " + js.dump() + " is not a string";
82 std::regex re;
83 try {
84 re.assign(js.get<std::string>());
85 } catch (const std::regex_error& e) {
86 return "bad NodeMatch " + js.dump() + ": " + e.what();
87 }
88 for (const NodeRef& node : hierarchy.Layout().ByNode())
89 if (node.IsComposite() && std::regex_match(node.name, re))
90 nodes.push_back(&node);
91 return "";
92}
93} // anonymous namespace
94
95Bool_t BmnAlignConfig::ReadSolution(const char* jsonPath, const BmnAlignHierarchy& hierarchy, Solution& solution)
96{
97 solution = Solution{};
98 solution.zeroComposites = hierarchy.CompositeCount() > 0;
99
100 std::ifstream ifs(jsonPath);
101 if (!ifs.good()) {
102 LOG(error) << " <!> BmnAlignConfig::ReadSolution: failed to open JSON file";
103 return kFALSE;
104 }
105 using namespace nlohmann;
106 json js;
107 try {
108 ifs >> js;
109
110 // check if solution is suitable
111 json jsAlignmentType = BmnParamLayout::ParamNames();
112 if (js.contains("CorrectionValues")) {
113 if (js["CorrectionValues"] != jsAlignmentType) {
114 LOG(error) << " <!> ReadSolution: wrong alignment type in JSON file\n"
115 << " expected: " << jsAlignmentType << ", in file: " << js["CorrectionValues"];
116 return kFALSE;
117 } else
118 LOG(info) << " " << js["CorrectionValues"] << " : alignment type checked";
119 } else
120 LOG(warning) << " ! CorrectionValues key is missing: assuming alignment type is correct";
121 const Int_t elements{hierarchy.LeafCount()};
122 if (js.contains("DetectorElements")) {
123 if (static_cast<Int_t>(js["DetectorElements"]) != elements) {
124 LOG(error) << " <!> ReadSolution: wrong detector configuration in JSON file\n"
125 << " modules expected: " << elements << ", in file: " << js["DetectorElements"];
126 return kFALSE;
127 } else
128 LOG(info) << " " << js["DetectorElements"] << " : detector elements count checked";
129 } else
130 LOG(warning) << " ! DetectorElements key is missing: assuming detector configuration is correct";
131 if (!js.contains("CorrectionsPerDetector") || !js["CorrectionsPerDetector"].is_array()) {
132 LOG(error) << " ReadSolution: CorrectionsPerDetector key is missing or not an array";
133 return kFALSE;
134 }
135
136 // read solution for each stored detector element
137 Int_t elementsCount{static_cast<Int_t>(js["CorrectionsPerDetector"].size())};
138 LOG_IF(warning, elementsCount != elements)
139 << Form(" ! %d detector elements found in file, %d was expected", elementsCount, elements);
140 LOG_IF(warning, elementsCount > elements) << " ! additional data validation is advised";
141 LOG_IF(warning, elementsCount < elements) << " ! assuming zero values for missing detector elements";
142 for (Int_t i = 0; i < elementsCount; i++) {
143 json& element = js["CorrectionsPerDetector"][i];
144 if (!element.contains("detector_id")) {
145 LOG(warning) << " ! DetectorID key is missing for some element, skipping.";
146 continue;
147 }
148 Int_t detID = element["detector_id"].get<Int_t>();
149 if (hierarchy.DetModel()->UnknownID(detID)) {
150 LOG(warning) << " ! Detector element ID " << detID << " (" << hierarchy.DetModel()->DecodeID(detID)
151 << ")"
152 << " is unknown or not used in current configuration, skipping";
153 continue;
154 }
155 if (!element.contains("values") || !element["values"].is_array()
156 || element["values"].size() != BMN_GLOBAL_PARAMS_PD)
157 {
158 LOG(warning) << " ! Correction values for the element " << detID << " ("
159 << hierarchy.DetModel()->DecodeID(detID) << ")"
160 << " are invalid, skipping.";
161 continue;
162 }
163 const Int_t detIdx{hierarchy.DetModel()->Idx(detID)};
164 NodeValues leaf;
165 leaf.node = detIdx;
166 json& values = element["values"];
167 for (Int_t j = 0; j < BMN_GLOBAL_PARAMS_PD; j++)
168 leaf.values(j) = values[j].get<Double_t>();
169 if (detID == hierarchy.DetModel()->TargetID()) // see the target note in the header
170 leaf.values(idZ) += hierarchy.DetModel()->TargetShift();
171 solution.leaves.push_back(leaf);
172 // "errors" key (if present in the file) is intentionally ignored
173 }
174 LOG(info) << " " << solution.leaves.size() << " detector elements loaded from JSON file";
175
176 // Composite node parameters, see WriteSolution. Matched by name, so a
177 // file written for a different hierarchy is reported node by node
178 // instead of being misapplied by position. An absent or partial block
179 // leaves the corresponding parameters at zero: the geometry loaded above
180 // is unaffected, the whole of it just counts as the elements' own - which
181 // is exactly what a file written before the levels existed means.
182 const Int_t nCompNodes{hierarchy.CompositeCount()};
183 if (nCompNodes) {
184 if (js.contains("CompositeNodes") && js["CompositeNodes"].is_array())
185 for (const json& entry : js["CompositeNodes"]) {
186 if (!entry.contains("name") || !entry.contains("values") || !entry["values"].is_array()
187 || entry["values"].size() != BMN_GLOBAL_PARAMS_PD)
188 {
189 LOG(warning) << " ! malformed composite node entry, skipping";
190 continue;
191 }
192 const std::string name{entry["name"].get<std::string>()};
193 const NodeRef* node{hierarchy.Layout().Find(name)};
194 if (!node || !node->IsComposite()) {
195 LOG(warning) << " ! '" << name
196 << "' is not a composite node of"
197 " the current hierarchy, skipping";
198 continue;
199 }
200 NodeValues comp;
201 comp.node = node->params[0].node;
202 for (const ParamRef& p : node->params)
203 comp.values(p.prm) = entry["values"][p.prm].get<Double_t>();
204 solution.composites.push_back(comp);
205 }
206 LOG(info) << " " << solution.composites.size() << " of " << nCompNodes
207 << " composite node(s) loaded from JSON file";
208 LOG_IF(warning, (Int_t)solution.composites.size() < nCompNodes)
209 << " ! the rest start at zero, so their share of the corrections"
210 " counts as the elements' own until the next iteration";
211 }
212
213 // TODO: load tracks approximation form JSON file if available
214 } catch (std::exception& e) {
215 LOG(error) << " <!> ReadSolution: failed to parse JSON file " << jsonPath;
216 LOG(error) << e.what();
217 return kFALSE;
218 }
219 return kTRUE;
220}
221
222Bool_t BmnAlignConfig::LoadConstraints(const char* jsonPath, const BmnAlignHierarchy& hierarchy, BmnAlignPriors& priors)
223{
224 /*
225 * JSON schema for constraints file
226 * --------------------------------
227 * Top-level value: array of entries. An entry is either one constraint
228 * written out coefficient by coefficient (constraint object), or a typed
229 * object the loader turns into a group of constraints.
230 *
231 * Detector elements are named by DetectorID, never by their index: an
232 * index means a different element in every model (full, half, without
233 * the target), and nothing could tell a file written for another model.
234 *
235 * Constraint object:
236 * - "Coefficients" (array, required): list of coefficient objects.
237 * - any other string-valued attribute (e.g. "Comment"): ignored. Used for
238 * comments. Order of attributes is irrelevant.
239 * - "IDComment" (optional, any value): ignored. service/comment_ids.C
240 * writes the text form of the DetectorIDs there, in any object type.
241 *
242 * Coefficient object (one of two mutually exclusive schemes per constraint):
243 *
244 * Scheme A (locator) — must use *exactly one* of {ParamName, ParamIndex}
245 * and *exactly one* of {DetectorID, NodeName}:
246 * { "ParamName": "dX", "DetectorID": 201392146, "Value": 1.0 }
247 * { "ParamIndex": 0, "DetectorID": 201392146, "Value": 1.0 }
248 * { "ParamName": "theta","NodeName": "GEM", "Value": 1.0 }
249 *
250 * NodeName addresses a composite node of the hierarchy (a leaf may also be
251 * named this way). Note the difference in meaning: a coefficient on a leaf
252 * parameter constrains that element's *effective* correction, the sum of
253 * its own parameters and everything its ancestors contribute, while a
254 * coefficient on a composite constrains that composite's own parameters.
255 * Leaf coefficients therefore keep working unchanged when a hierarchy is
256 * introduced - they still fix the same physical degree of freedom.
257 *
258 * Scheme B (direct):
259 * { "ConstrIndex": 17, "Value": 1.0 } // 0..AllParams()-1
260 *
261 * RHS: // at most one per constraint
262 * { "RHS": 0.0 } // no other attributes allowed
263 *
264 * Mixing schemes within one constraint is an error.
265 * Missing coefficients are zero (no warning).
266 *
267 * Typed object: the loader builds the constraints from the hierarchy and
268 * the detector model, so one entry stays right for every model it is used
269 * with - full or half, flat or hierarchical, with or without the target.
270 * - "Type" (required): Fix | Sum | Shear | NodeEqual, see below.
271 * - "ParamName" (required): one name or an array of names. The entry is
272 * applied to each of them in turn.
273 * - exactly one of:
274 * "Over": "Elements" | "Tracker" | "Target" - detector elements:
275 * all, all but the target, the target only
276 * "NodeMatch": regular expression; selects the composites whose
277 * whole name matches it, e.g. "STA0[6-9][UL]"
278 * "ID": array of DetectorIDs, NodeEqual only
279 * - "RHS" (optional, not for NodeEqual): the right-hand side of every row
280 * made, 0.0 if absent.
281 * - "Comment", "IDComment" (optional). Nothing else is allowed: in a
282 * typed object a string value is not a comment, so a misspelt key
283 * cannot vanish.
284 *
285 * Type rows made each row says
286 * Fix one per object P = RHS
287 * Sum one sum of P over the objects = RHS
288 * Shear one, Over only sum of w_i P_i = RHS
289 * NodeEqual n-1 per group of n elements P of an element = P of the first one
290 *
291 * Shear weights are w_i = (z_i - z_mean) / max |z_i - z_mean|, with z of
292 * each element from the detector model geometry. Straight tracks do not
293 * see a dX or dY growing linearly with z: it goes into their slopes. Lin3
294 * sees dZ only as -dZ * slope, so a dZ growing linearly with z (a scale)
295 * goes the same way.
296 * A NodeEqual group is the elements anywhere below one matched composite,
297 * or the ID list. A group of a single element makes no rows and is
298 * reported.
299 *
300 * As for coefficients, rows on elements constrain their effective
301 * corrections, rows on composites their own parameters.
302 * An entry that addresses nothing in the current model makes no rows,
303 * only a warning, so one file can serve several models. Each typed entry
304 * logs the numbers of the rows it made: later messages (BmnAlignPriors,
305 * BmnAlignReport) count rows, not entries.
306 */
307 if (!jsonPath) {
308 LOG(warning) << " ! constraints file in not provided, proceeding without constraints";
309 return kTRUE;
310 }
311 LOG(info) << " " << jsonPath << " : constraints file provided";
312 std::ifstream ifs(jsonPath);
313 if (!ifs.good()) {
314 LOG(error) << " <!> BmnAlignConfig::LoadConstraints: failed to open JSON file " << jsonPath;
315 return kFALSE;
316 }
317 nlohmann::json jsonData;
318 try {
319 ifs >> jsonData;
320 } catch (const std::exception& e) {
321 LOG(error) << " <!> BmnAlignConfig::LoadConstraints: failed to parse JSON file " << jsonPath;
322 LOG(error) << e.what();
323 return kFALSE;
324 }
325 if (!jsonData.is_array()) {
326 LOG(error) << " <!> BmnAlignConfig::LoadConstraints: top-level JSON is not an array in " << jsonPath;
327 return kFALSE;
328 }
329 const Int_t entriesCnt{static_cast<Int_t>(jsonData.size())};
330 if (entriesCnt <= 0) {
331 LOG(error) << " <!> BmnAlignConfig::LoadConstraints: empty constraints array in " << jsonPath;
332 return kFALSE;
333 }
334
335 auto detIdxByID = [&hierarchy](Int_t encID) -> Int_t {
336 try {
337 return hierarchy.DetModel()->Idx(encID);
338 } catch (const std::out_of_range&) {
339 return -1;
340 }
341 };
342 auto isCoefKey = [](const std::string& k) -> Bool_t {
343 return k == "ParamName" || k == "ParamIndex" || k == "DetectorID" || k == "NodeName" || k == "ConstrIndex"
344 || k == "Value" || k == "RHS";
345 };
346 auto isTypedKey = [](const std::string& k) -> Bool_t {
347 return k == "Type" || k == "ParamName" || k == "Over" || k == "NodeMatch" || k == "ID" || k == "RHS"
348 || k == "Comment" || k == "IDComment";
349 };
350
351 // a typed entry makes any number of rows, so they are collected first
352 const Int_t nFull{hierarchy.AllParams()};
353 std::vector<TVectorD> rows;
354 VecDouble_t rowsRhs;
355 Int_t errors{0};
356 auto addRow = [&rows, &rowsRhs](const TVectorD& row, Double_t rhs) {
357 rows.push_back(row);
358 rowsRhs.push_back(rhs);
359 };
360 priors.inhomogeneous = kFALSE;
361 auto noteRhs = [&priors](Double_t rhs, Int_t entry) {
362 if (std::fabs(rhs) <= SOFT_ZERO)
363 return;
364 priors.inhomogeneous = kTRUE;
365 LOG(warning) << " ! constraint #" << entry
366 << " has non-zero RHS -- (unlikely) violation is possible"
367 " (see comment in BmnAligner::SolveGlobal)";
368 };
369
370 for (Int_t i = 0; i < entriesCnt; i++) {
371 const auto& jsConstr = jsonData[i];
372 if (!jsConstr.is_object()) {
373 LOG(error) << " <!> BmnAlignConfig::LoadConstraints: constraint #" << i + 1 << " is not an object";
374 errors++;
375 continue;
376 }
377
378 // ==== typed object ====
379 if (jsConstr.contains("Type")) {
380 Int_t entryErrors{0};
381 auto fail = [&](const std::string& what) {
382 LOG(error) << " <!> BmnAlignConfig::LoadConstraints: constraint #" << i + 1 << " : " << what;
383 entryErrors++;
384 };
385 for (auto it = jsConstr.begin(); it != jsConstr.end(); ++it)
386 if (!isTypedKey(it.key()))
387 fail("unexpected attribute '" + it.key() + "'");
388 const nlohmann::json& jsType{jsConstr.at("Type")};
389 const std::string type{jsType.is_string() ? jsType.get<std::string>() : ""};
390 const Bool_t fix{type == "Fix"}, sum{type == "Sum"}, shear{type == "Shear"}, equal{type == "NodeEqual"};
391 if (!fix && !sum && !shear && !equal)
392 fail("unknown Type " + jsType.dump() + ", expected Fix / Sum / Shear / NodeEqual");
393 VecInt_t params;
394 if (!jsConstr.contains("ParamName"))
395 fail("ParamName is required");
396 else if (const std::string what{ParamList(jsConstr.at("ParamName"), params)}; !what.empty())
397 fail(what);
398 const Bool_t hasOver{jsConstr.contains("Over")}, hasMatch{jsConstr.contains("NodeMatch")},
399 hasID{jsConstr.contains("ID")}, hasRHS{jsConstr.contains("RHS")};
400 if (hasOver + hasMatch + hasID != 1)
401 fail("exactly one of Over / NodeMatch / ID required");
402 if (hasID && !equal)
403 fail("ID is for NodeEqual only");
404 if (hasOver && equal)
405 fail("NodeEqual takes NodeMatch or ID, not Over");
406 if (hasMatch && shear)
407 fail("Shear weighs elements by their z, so it takes Over, not NodeMatch");
408 if (hasRHS && equal)
409 fail("NodeEqual takes no RHS");
410 if (hasRHS && !jsConstr.at("RHS").is_number())
411 fail("RHS " + jsConstr.at("RHS").dump() + " is not a number");
412 if (entryErrors) {
413 errors += entryErrors;
414 continue;
415 }
416 const Double_t rhs{hasRHS ? jsConstr.at("RHS").get<Double_t>() : 0.0};
417
418 // the addressed objects; only NodeEqual has more than one group of them
419 std::vector<NodeList_t> groups(1);
420 std::string what;
421 if (hasOver)
422 what = LeafScope(jsConstr.at("Over"), hierarchy, groups[0]);
423 else if (hasMatch)
424 what = MatchComposites(jsConstr.at("NodeMatch"), hierarchy, groups[0]);
425 else if (const nlohmann::json & ids{jsConstr.at("ID")}; !ids.is_array())
426 what = "ID " + ids.dump() + " is not an array";
427 else
428 for (const nlohmann::json& id : ids) {
429 const Int_t detIdx{id.is_number_integer() ? detIdxByID(id.get<Int_t>()) : -1};
430 if (detIdx < 0) {
431 what = "unknown DetectorID " + id.dump();
432 if (id.is_number_integer())
433 what += " (" + hierarchy.DetModel()->DecodeID(id.get<Int_t>()) + ")";
434 break;
435 }
436 if (std::find(groups[0].begin(), groups[0].end(), &hierarchy.Layout().ByNode()[detIdx])
437 != groups[0].end())
438 {
439 what = "ID lists " + id.dump() + " twice";
440 break;
441 }
442 groups[0].push_back(&hierarchy.Layout().ByNode()[detIdx]);
443 }
444 if (what.empty() && shear
445 && hierarchy.DetModel()->Geometry().size() != static_cast<std::size_t>(hierarchy.LeafCount()))
446 what = "Shear needs the element z, but the detector model has no geometry";
447 if (!what.empty()) {
448 fail(what);
449 errors += entryErrors;
450 continue;
451 }
452
453 if (equal && hasMatch) { // one group per matched composite: the elements anywhere below it
454 const NodeList_t composites{groups[0]};
455 groups.clear();
456 for (const NodeRef* comp : composites) {
457 NodeList_t group;
458 for (const NodeRef& node : hierarchy.Layout().ByNode()) {
459 if (node.IsComposite())
460 continue;
461 for (Int_t n = hierarchy.Nodes()[node.leaf].parent; n >= 0; n = hierarchy.Nodes()[n].parent)
462 if (hierarchy.Nodes()[n].name == comp->name) {
463 group.push_back(&node);
464 break;
465 }
466 }
467 LOG_IF(warning, group.size() < 2) << " ! constraint #" << i + 1 << " : composite '" << comp->name
468 << "' holds a single element, skipped";
469 if (group.size() >= 2)
470 groups.push_back(group);
471 }
472 } else if (equal && groups[0].size() < 2) {
473 LOG(warning) << " ! constraint #" << i + 1 << " : the ID list names a single element, skipped";
474 groups.clear();
475 }
476
477 VecDouble_t weight; // Shear only, in the order of groups[0]
478 if (shear && !groups[0].empty()) {
479 const auto& geometry{hierarchy.DetModel()->Geometry()};
480 Double_t zMean{0.0}, spread{0.0};
481 for (const NodeRef* node : groups[0])
482 zMean += geometry[node->leaf].z;
483 zMean /= groups[0].size();
484 for (const NodeRef* node : groups[0])
485 spread = std::max(spread, std::fabs(geometry[node->leaf].z - zMean));
486 if (!(spread > 0.0)) {
487 fail("Shear over elements which all lie at one z");
488 errors += entryErrors;
489 continue;
490 }
491 for (const NodeRef* node : groups[0])
492 weight.push_back((geometry[node->leaf].z - zMean) / spread);
493 }
494
495 const std::size_t firstRow{rows.size()};
496 for (const Int_t p : params) {
497 if (fix)
498 for (const NodeRef* node : groups[0]) {
499 TVectorD row(nFull);
500 row(node->params[p].full) = 1.0;
501 addRow(row, rhs);
502 }
503 if ((sum || shear) && !groups[0].empty()) {
504 TVectorD row(nFull);
505 for (std::size_t k = 0; k < groups[0].size(); k++)
506 row(groups[0][k]->params[p].full) = shear ? weight[k] : 1.0;
507 addRow(row, rhs);
508 }
509 if (equal)
510 for (const NodeList_t& group : groups)
511 for (std::size_t k = 1; k < group.size(); k++) {
512 TVectorD row(nFull);
513 row(group[0]->params[p].full) = -1.0;
514 row(group[k]->params[p].full) = 1.0;
515 addRow(row, 0.0);
516 }
517 }
518 if (rows.size() == firstRow) {
519 LOG(warning) << " ! constraint #" << i + 1 << " (" << type
520 << ") addresses nothing in this model, no rows made";
521 continue;
522 }
523 noteRhs(rhs, i + 1);
524 LOG(info) << Form(" constraint #%d (%s) : %zu row(s), rows #%zu..#%zu", i + 1, type.c_str(),
525 rows.size() - firstRow, firstRow + 1, rows.size());
526 continue;
527 }
528
529 // ==== constraint object ====
530 const nlohmann::json* jsCoeffs{nullptr};
531 for (auto it = jsConstr.begin(); it != jsConstr.end(); ++it) {
532 if (it.value().is_string() || it.key() == "IDComment")
533 continue; // comment-like attribute, ignored
534 if (it.key() == "Coefficients" && it.value().is_array())
535 jsCoeffs = &it.value();
536 else {
537 LOG(error) << " <!> BmnAlignConfig::LoadConstraints: constraint #" << i + 1
538 << " has unexpected attribute '" << it.key() << "'";
539 errors++;
540 }
541 }
542 if (!jsCoeffs) {
543 LOG(error) << " <!> BmnAlignConfig::LoadConstraints: constraint #" << i + 1
544 << " has no 'Coefficients' array";
545 errors++;
546 continue;
547 }
548
549 enum Scheme
550 {
551 Unknown,
552 Locator,
553 Direct
554 };
555 Scheme scheme{Unknown};
556 Bool_t hasRHS{kFALSE};
557 Bool_t anyLhs{kFALSE};
558 TVectorD row(nFull);
559 Double_t rowRhs{0.0};
560
561 for (Int_t cIdx = 0; cIdx < (Int_t)jsCoeffs->size(); cIdx++) {
562 const auto& jsCoef = (*jsCoeffs)[cIdx];
563 if (!jsCoef.is_object()) {
564 LOG(error) << " <!> BmnAlignConfig::LoadConstraints: constraint #" << i + 1 << ", coefficient #"
565 << cIdx + 1 << " is not an object";
566 errors++;
567 continue;
568 }
569 // Reject unknown keys inside a coefficient (typo guard).
570 Bool_t badKey{kFALSE};
571 for (auto it = jsCoef.begin(); it != jsCoef.end(); ++it)
572 if (!isCoefKey(it.key())) {
573 LOG(error) << " <!> BmnAlignConfig::LoadConstraints: constraint #" << i + 1 << ", coefficient #"
574 << cIdx + 1 << " has unknown attribute '" << it.key() << "'";
575 badKey = kTRUE;
576 }
577 if (badKey) {
578 errors++;
579 continue;
580 }
581
582 const Bool_t hasParamName{jsCoef.contains("ParamName")};
583 const Bool_t hasParamIndex{jsCoef.contains("ParamIndex")};
584 const Bool_t hasDetectorID{jsCoef.contains("DetectorID")};
585 const Bool_t hasNodeName{jsCoef.contains("NodeName")};
586 const Bool_t hasConstrIndex{jsCoef.contains("ConstrIndex")};
587 const Bool_t hasValue{jsCoef.contains("Value")};
588 const Bool_t hasRHSKey{jsCoef.contains("RHS")};
589
590 // ---- RHS coefficient ----
591 if (hasRHSKey) {
592 if (hasParamName || hasParamIndex || hasDetectorID || hasNodeName || hasConstrIndex || hasValue) {
593 LOG(error) << " <!> BmnAlignConfig::LoadConstraints: constraint #" << i + 1 << ", coefficient #"
594 << cIdx + 1 << " has RHS together with other attributes";
595 errors++;
596 continue;
597 }
598 if (hasRHS) {
599 LOG(error) << " <!> BmnAlignConfig::LoadConstraints: constraint #" << i + 1
600 << " has multiple RHS entries";
601 errors++;
602 continue;
603 }
604 rowRhs = jsCoef["RHS"].get<Double_t>();
605 noteRhs(rowRhs, i + 1);
606 hasRHS = kTRUE;
607 continue;
608 }
609
610 // ---- LHS coefficient ----
611 if (!hasValue) {
612 LOG(error) << " <!> BmnAlignConfig::LoadConstraints: constraint #" << i + 1 << ", coefficient #"
613 << cIdx + 1 << " has neither Value nor RHS";
614 errors++;
615 continue;
616 }
617 const Double_t value{jsCoef["Value"].get<Double_t>()};
618
619 if (hasConstrIndex) {
620 if (hasParamName || hasParamIndex || hasDetectorID || hasNodeName) {
621 LOG(error) << " <!> BmnAlignConfig::LoadConstraints: constraint #" << i + 1 << ", coefficient #"
622 << cIdx + 1 << " mixes ConstrIndex with locator attributes";
623 errors++;
624 continue;
625 }
626 if (scheme == Locator) {
627 LOG(error) << " <!> BmnAlignConfig::LoadConstraints: constraint #" << i + 1
628 << " mixes locator and direct schemes";
629 errors++;
630 continue;
631 }
632 scheme = Direct;
633 const Int_t idx{jsCoef["ConstrIndex"].get<Int_t>()};
634 if (idx < 0 || idx >= hierarchy.AllParams()) {
635 LOG(error) << " <!> BmnAlignConfig::LoadConstraints: constraint #" << i + 1 << ", coefficient #"
636 << cIdx + 1 << " : ConstrIndex " << idx << " out of range [0, " << hierarchy.AllParams()
637 << ")";
638 errors++;
639 continue;
640 }
641 row(idx) = value;
642 anyLhs = kTRUE;
643 continue;
644 }
645
646 // locator scheme
647 if (hasParamName == hasParamIndex) {
648 LOG(error) << " <!> BmnAlignConfig::LoadConstraints: constraint #" << i + 1 << ", coefficient #"
649 << cIdx + 1 << " : exactly one of ParamName / ParamIndex required";
650 errors++;
651 continue;
652 }
653 if (hasDetectorID + hasNodeName != 1) {
654 LOG(error) << " <!> BmnAlignConfig::LoadConstraints: constraint #" << i + 1 << ", coefficient #"
655 << cIdx + 1 << " : exactly one of DetectorID / NodeName required";
656 errors++;
657 continue;
658 }
659 if (scheme == Direct) {
660 LOG(error) << " <!> BmnAlignConfig::LoadConstraints: constraint #" << i + 1
661 << " mixes locator and direct schemes";
662 errors++;
663 continue;
664 }
665 scheme = Locator;
666
667 Int_t paramIdx{-1};
668 if (hasParamName) {
669 const std::string name{jsCoef["ParamName"].get<std::string>()};
670 paramIdx = BmnParamLayout::ParamIndex(name);
671 if (paramIdx < 0) {
672 LOG(error) << " <!> BmnAlignConfig::LoadConstraints: constraint #" << i + 1 << ", coefficient #"
673 << cIdx + 1 << " : unknown ParamName '" << name << "'";
674 errors++;
675 continue;
676 }
677 } else {
678 paramIdx = jsCoef["ParamIndex"].get<Int_t>();
679 if (paramIdx < 0 || paramIdx >= BMN_GLOBAL_PARAMS_PD) {
680 LOG(error) << " <!> BmnAlignConfig::LoadConstraints: constraint #" << i + 1 << ", coefficient #"
681 << cIdx + 1 << " : ParamIndex " << paramIdx << " out of range [0, "
682 << BMN_GLOBAL_PARAMS_PD << ")";
683 errors++;
684 continue;
685 }
686 }
687
688 // the addressed object, however the entry chose to name it
689 const NodeRef* target{nullptr};
690 if (hasNodeName) {
691 const std::string name{jsCoef["NodeName"].get<std::string>()};
692 target = hierarchy.Layout().Find(name);
693 if (!target) {
694 LOG(error) << " <!> BmnAlignConfig::LoadConstraints: constraint #" << i + 1 << ", coefficient #"
695 << cIdx + 1 << " : unknown NodeName '" << name << "'";
696 errors++;
697 continue;
698 }
699 } else {
700 const Int_t encID{jsCoef["DetectorID"].get<Int_t>()};
701 const Int_t detIdx{detIdxByID(encID)};
702 if (detIdx < 0) {
703 LOG(error) << " <!> BmnAlignConfig::LoadConstraints: constraint #" << i + 1 << ", coefficient #"
704 << cIdx + 1 << " : unknown DetectorID " << encID << " ("
705 << hierarchy.DetModel()->DecodeID(encID) << ")";
706 errors++;
707 continue;
708 }
709 target = &hierarchy.Layout().ByNode()[detIdx];
710 }
711 row(target->params[paramIdx].full) = value;
712 anyLhs = kTRUE;
713 }
714 if (!anyLhs) {
715 LOG(warning) << " ! constraint #" << i + 1 << " has all-zero LHS - it is either trivial or unsatisfiable";
716 }
717 addRow(row, rowRhs);
718 }
719
720 if (errors) {
721 LOG(error) << " <!> BmnAlignConfig::LoadConstraints: " << errors << " error(s) detected, aborting";
722 return kFALSE;
723 }
724 const Int_t rowsCnt{static_cast<Int_t>(rows.size())};
725 priors.constraints.ResizeTo(rowsCnt, nFull);
726 priors.rhs.ResizeTo(rowsCnt);
727 for (Int_t r = 0; r < rowsCnt; r++) {
728 TMatrixDRow(priors.constraints, r) = rows[r];
729 priors.rhs(r) = rowsRhs[r];
730 }
731 LOG(info) << " " << rowsCnt << " constraint row(s) loaded from " << entriesCnt << " entries";
732 return kTRUE;
733}
734
735Bool_t BmnAlignConfig::LoadPresigmas(const char* jsonPath, const BmnAlignHierarchy& hierarchy, BmnAlignPriors& priors)
736{
737 /*
738 * JSON schema for presigmas file
739 * ------------------------------
740 * Top-level value: array of presigma objects. Each object attaches a prior
741 * uncertainty to one global parameter (of one detector element, or of all
742 * of them at once), turning it into a pseudo-measurement of that parameter
743 * - see BmnAligner::ApplyPresigmas for the mathematics.
744 *
745 * Presigma object:
746 * - exactly one of {ParamName, ParamIndex} (required): which correction.
747 * "ParamName": "dX" | "dY" | "dZ" | "phi" | "theta" | "psi", or an
748 * array of them for the same prior on each
749 * "ParamIndex": 0..BMN_GLOBAL_PARAMS_PD-1
750 * - at most one of {DetectorID, NodeName, Over, NodeMatch} (optional):
751 * which objects. Elements are named by DetectorID, never by index
752 * (see LoadConstraints).
753 * "NodeName" addresses one hierarchy node (a composite, or a leaf).
754 * "Over" "Elements" | "Tracker" | "Target": detector elements -
755 * all, all but the target, the target only.
756 * "NodeMatch" regular expression: the composites whose whole name
757 * matches it.
758 * If all are absent, the entry applies to every detector element,
759 * like "Over": "Elements". Composite nodes are reached by NodeName
760 * or NodeMatch only, so that adding a level cannot silently shift
761 * the balance between levels. An entry that addresses nothing in
762 * the current model is reported and has no effect.
763 * - "Sigma" (required, > 0): prior uncertainty, in the units of the
764 * parameter (cm for dX/dY/dZ, rad for phi/theta/psi).
765 * - "Ref" (optional, default 0.0): value the prior pulls towards, in the
766 * same units. Zero means the nominal (starting) geometry.
767 * - any other string-valued attribute (e.g. "Comment"): ignored. Used for
768 * comments. Order of attributes is irrelevant.
769 * - "IDComment" (optional, any value): ignored, see LoadConstraints.
770 *
771 * Later entries override earlier ones for the same parameter, so a
772 * detector-wide default may be stated first and then refined per element.
773 * Parameters never mentioned get no prior at all.
774 */
775 if (!jsonPath) {
776 LOG(info) << " presigmas file is not provided, proceeding without priors";
777 return kTRUE;
778 }
779 LOG(info) << " " << jsonPath << " : presigmas file provided";
780 std::ifstream ifs(jsonPath);
781 if (!ifs.good()) {
782 LOG(error) << " <!> BmnAlignConfig::LoadPresigmas: failed to open JSON file " << jsonPath;
783 return kFALSE;
784 }
785 nlohmann::json jsonData;
786 try {
787 ifs >> jsonData;
788 } catch (const std::exception& e) {
789 LOG(error) << " <!> BmnAlignConfig::LoadPresigmas: failed to parse JSON file " << jsonPath;
790 LOG(error) << e.what();
791 return kFALSE;
792 }
793 if (!jsonData.is_array()) {
794 LOG(error) << " <!> BmnAlignConfig::LoadPresigmas: top-level JSON is not an array in " << jsonPath;
795 return kFALSE;
796 }
797 const Int_t entriesCnt = jsonData.size();
798 if (entriesCnt <= 0) {
799 LOG(error) << " <!> BmnAlignConfig::LoadPresigmas: empty presigmas array in " << jsonPath;
800 return kFALSE;
801 }
802
803 auto detIdxByID = [&hierarchy](Int_t encID) -> Int_t {
804 try {
805 return hierarchy.DetModel()->Idx(encID);
806 } catch (const std::out_of_range&) {
807 return -1;
808 }
809 };
810 auto isPresigmaKey = [](const std::string& k) -> Bool_t {
811 return k == "ParamName" || k == "ParamIndex" || k == "DetectorID" || k == "NodeName" || k == "Over"
812 || k == "NodeMatch" || k == "Sigma" || k == "Ref" || k == "IDComment";
813 };
814
815 priors.sigma.assign(hierarchy.AllParams(), 0.0);
816 priors.ref.assign(hierarchy.AllParams(), 0.0);
817 Int_t errors{0};
818
819 for (Int_t i = 0; i < entriesCnt; i++) {
820 const auto& jsEntry = jsonData[i];
821 if (!jsEntry.is_object()) {
822 LOG(error) << " <!> BmnAlignConfig::LoadPresigmas: presigma #" << i + 1 << " is not an object";
823 errors++;
824 continue;
825 }
826 // Reject unknown keys (typo guard), string values are comment-like
827 Bool_t badKey{kFALSE};
828 for (auto it = jsEntry.begin(); it != jsEntry.end(); ++it) {
829 if (it.value().is_string())
830 continue; // comment-like attribute, ignored
831 if (!isPresigmaKey(it.key())) {
832 LOG(error) << " <!> BmnAlignConfig::LoadPresigmas: presigma #" << i + 1 << " has unknown attribute '"
833 << it.key() << "'";
834 badKey = kTRUE;
835 }
836 }
837 if (badKey) {
838 errors++;
839 continue;
840 }
841
842 const Bool_t hasParamName{jsEntry.contains("ParamName")};
843 const Bool_t hasParamIndex{jsEntry.contains("ParamIndex")};
844 const Bool_t hasDetectorID{jsEntry.contains("DetectorID")};
845 const Bool_t hasNodeName{jsEntry.contains("NodeName")};
846 const Bool_t hasOver{jsEntry.contains("Over")};
847 const Bool_t hasMatch{jsEntry.contains("NodeMatch")};
848
849 if (hasParamName == hasParamIndex) {
850 LOG(error) << " <!> BmnAlignConfig::LoadPresigmas: presigma #" << i + 1
851 << " : exactly one of ParamName / ParamIndex required";
852 errors++;
853 continue;
854 }
855 if (hasDetectorID + hasNodeName + hasOver + hasMatch > 1) {
856 LOG(error) << " <!> BmnAlignConfig::LoadPresigmas: presigma #" << i + 1
857 << " : at most one of DetectorID / NodeName / Over / NodeMatch allowed";
858 errors++;
859 continue;
860 }
861 if (!jsEntry.contains("Sigma")) {
862 LOG(error) << " <!> BmnAlignConfig::LoadPresigmas: presigma #" << i + 1 << " has no Sigma";
863 errors++;
864 continue;
865 }
866 const Double_t sigma{jsEntry["Sigma"].get<Double_t>()};
867 if (!(sigma > 0.0)) {
868 LOG(error) << " <!> BmnAlignConfig::LoadPresigmas: presigma #" << i + 1 << " : Sigma must be positive, got "
869 << sigma;
870 errors++;
871 continue;
872 }
873 const Double_t ref{jsEntry.contains("Ref") ? jsEntry["Ref"].get<Double_t>() : 0.0};
874
875 VecInt_t params;
876 if (hasParamName) {
877 const std::string what{ParamList(jsEntry["ParamName"], params)};
878 if (!what.empty()) {
879 LOG(error) << " <!> BmnAlignConfig::LoadPresigmas: presigma #" << i + 1 << " : " << what;
880 errors++;
881 continue;
882 }
883 } else {
884 const Int_t paramIdx{jsEntry["ParamIndex"].get<Int_t>()};
885 if (paramIdx < 0 || paramIdx >= BMN_GLOBAL_PARAMS_PD) {
886 LOG(error) << " <!> BmnAlignConfig::LoadPresigmas: presigma #" << i + 1 << " : ParamIndex " << paramIdx
887 << " out of range [0, " << BMN_GLOBAL_PARAMS_PD << ")";
888 errors++;
889 continue;
890 }
891 params.push_back(paramIdx);
892 }
893
894 NodeList_t nodes;
895 std::string what;
896 if (hasNodeName) {
897 const std::string name{jsEntry["NodeName"].get<std::string>()};
898 if (const NodeRef * node{hierarchy.Layout().Find(name)})
899 nodes.push_back(node);
900 else
901 what = "unknown NodeName '" + name + "'";
902 } else if (hasDetectorID) {
903 const Int_t encID{jsEntry["DetectorID"].get<Int_t>()};
904 const Int_t detIdx{detIdxByID(encID)};
905 if (detIdx >= 0)
906 nodes.push_back(&hierarchy.Layout().ByNode()[detIdx]);
907 else
908 what = Form("unknown DetectorID %d (%s)", encID, hierarchy.DetModel()->DecodeID(encID).c_str());
909 } else if (hasMatch)
910 what = MatchComposites(jsEntry["NodeMatch"], hierarchy, nodes);
911 else // no locator at all means every element, as "Over": "Elements" does
912 what = LeafScope(hasOver ? jsEntry["Over"] : nlohmann::json("Elements"), hierarchy, nodes);
913 if (!what.empty()) {
914 LOG(error) << " <!> BmnAlignConfig::LoadPresigmas: presigma #" << i + 1 << " : " << what;
915 errors++;
916 continue;
917 }
918 LOG_IF(warning, nodes.empty()) << " ! presigma #" << i + 1 << " addresses nothing in this model";
919
920 for (const Int_t p : params)
921 for (const NodeRef* node : nodes) {
922 priors.sigma[node->params[p].full] = sigma;
923 priors.ref[node->params[p].full] = ref;
924 }
925 }
926
927 if (errors) {
928 LOG(error) << " <!> BmnAlignConfig::LoadPresigmas: " << errors << " error(s) detected, aborting";
929 return kFALSE;
930 }
931
932 priors.presigmaCount = 0;
933 for (Double_t sigma : priors.sigma)
934 if (sigma > 0.0)
935 priors.presigmaCount++;
936 if (!priors.UsePresigmas()) {
937 LOG(warning) << " ! no parameter got a prior, proceeding without presigmas";
938 return kTRUE;
939 }
940
941 LOG(info) << " " << priors.presigmaCount << " of " << hierarchy.AllParams() << " parameters got a prior ("
942 << entriesCnt << " entries)";
943 std::array<Int_t, BMN_GLOBAL_PARAMS_PD> perCorrection{};
944 for (const ParamRef& p : hierarchy.Layout().Leaves())
945 if (priors.sigma[p.full] > 0.0)
946 perCorrection[p.prm]++;
947 for (Int_t p = 0; p < BMN_GLOBAL_PARAMS_PD; p++)
948 LOG_IF(info, perCorrection[p]) << " " << BmnParamLayout::ParamName(p) << " : " << perCorrection[p]
949 << " element(s) constrained";
950 for (const NodeRef& node : hierarchy.Layout().ByNode()) {
951 if (!node.IsComposite())
952 continue;
953 TString named;
954 for (const ParamRef& p : node.params)
955 if (priors.sigma[p.full] > 0.0)
956 named += Form("%s=%.3g ", p.prmName, priors.sigma[p.full]);
957 LOG(info) << " composite '" << node.name << "' : " << (named.IsNull() ? "no prior" : named.Data());
958 }
959 return kTRUE;
960}
961
963 const BmnAlignHierarchy& hierarchy,
964 const BmnAlignResult& result,
965 Int_t totalTracks,
966 Bool_t withAlpha)
967{
968 LOG(state) << " > BmnAlignConfig : saving result to file";
969 LOG(info) << " " << jsonPath << " : file to place result";
970
971 std::ofstream ofs(jsonPath);
972 if (!ofs.good()) {
973 LOG(error) << " <!> BmnAlignConfig::WriteSolution: failed to open output file";
974 return kFALSE;
975 }
976
977 using namespace nlohmann;
978 json js; // all data to be saved
979
980 // alignment result
981 js["CorrectionValues"] = BmnParamLayout::ParamNames();
982 js["DetectorElements"] = hierarchy.DetModel()->Alignables();
983 js["CorrectionsPerDetector"] = json::array();
984 // Composite nodes of the hierarchy. Not part of the geometry - the element
985 // corrections are the *effective* ones and already contain J * Pcomp. What
986 // the second block records is how that geometry splits between the levels,
987 // which is lost otherwise and cannot be recovered from the elements alone.
988 const TVectorD& solutionParams{result.A()};
989 if (hierarchy.CompositeCount())
990 js["CompositeNodes"] = json::array();
991 for (const NodeRef& node : hierarchy.Layout().ByNode()) {
992 json element;
993 element["values"] = json::array();
994 if (node.IsComposite()) {
995 element["name"] = node.name;
996 element["errors"] = json::array();
997 for (const ParamRef& p : node.params) {
998 element["values"].push_back(solutionParams(p.full));
999 element["errors"].push_back(result.AErr(p.full));
1000 }
1001 js["CompositeNodes"].push_back(element);
1002 } else {
1003 const Int_t detID{hierarchy.DetModel()->EncodedID(node.leaf)};
1004 SVectGL values{result.A(node.leaf)};
1005 if (detID == hierarchy.DetModel()->TargetID()) // see the target note in the header
1006 values(idZ) -= hierarchy.DetModel()->TargetShift();
1007 element["detector_id"] = detID;
1008 element["errors"] = json::array();
1009 for (const ParamRef& p : node.params) {
1010 element["values"].push_back(values(p.prm));
1011 element["errors"].push_back(result.AErr(p.full));
1012 }
1013 js["CorrectionsPerDetector"].push_back(element);
1014 }
1015 }
1016
1017 // tracks approximation
1018 if (withAlpha) {
1019 js["TracksCount"] = totalTracks;
1020 js["TracksParameters"] = json::array();
1021 for (const SVectLC& alpha : result.Alpha()) {
1022 json element = json::array();
1023 for (Int_t j = 0; j < BMN_LOCAL_PARAMS_PT; j++)
1024 element.push_back(alpha(j));
1025 js["TracksParameters"].push_back(element);
1026 }
1027 }
1028
1029 // write data
1030 try {
1031 ofs << std::setw(4) << js << std::endl;
1032 } catch (std::exception& e) {
1033 LOG(error) << " <!> BmnAlignConfig::WriteSolution: failed to write JSON data to file\n" << e.what();
1034 return kFALSE;
1035 }
1036
1037 LOG(state) << " < BmnAlignConfig : solution saved";
1038 LOG(state) << " ---";
1039 return kTRUE;
1040}
std::vector< Double_t > VecDouble_t
@ idZ
RM::SVector< Double_t, BMN_LOCAL_PARAMS_PT > SVectLC
RM::SVector< Double_t, BMN_GLOBAL_PARAMS_PD > SVectGL
std::vector< Int_t > VecInt_t
#define BMN_LOCAL_PARAMS_PT
#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
TMatrixTRow< Double_t > TMatrixDRow
char * Form(const char *fmt,...)
TVectorT< Double_t > TVectorD
static Bool_t LoadConstraints(const char *jsonPath, const BmnAlignHierarchy &hierarchy, BmnAlignPriors &priors)
static Bool_t WriteSolution(const char *jsonPath, const BmnAlignHierarchy &hierarchy, const BmnAlignResult &result, Int_t totalTracks, Bool_t withAlpha)
static Bool_t LoadPresigmas(const char *jsonPath, const BmnAlignHierarchy &hierarchy, BmnAlignPriors &priors)
static Bool_t ReadSolution(const char *jsonPath, const BmnAlignHierarchy &hierarchy, Solution &solution)
Int_t LeafCount() const noexcept
Int_t CompositeCount() const noexcept
const std::vector< Node_t > & Nodes() const noexcept
Int_t AllParams() const noexcept
const BmnParamLayout & Layout() const noexcept
const BmnDetectorModel * DetModel() const noexcept
VecDouble_t ref
VecDouble_t sigma
TMatrixD constraints
Bool_t UsePresigmas() const noexcept
SVectGL & A(Int_t index) noexcept
TVectorD & AErr() noexcept
VecSVectLC_t & Alpha() noexcept
const std::vector< Plane > & Geometry() const
Int_t EncodedID(Int_t idx) const
virtual std::string DecodeID(Int_t id) const =0
virtual Int_t Alignables() const =0
Bool_t UnknownID(Int_t hitID) const
virtual Int_t TargetID() const
Int_t Idx(Int_t ID) const
Double_t TargetShift() const noexcept
static Int_t ParamIndex(const std::string &name)
const Nodes_t & ByNode() const noexcept
static const char * ParamName(Int_t p)
const NodeRef * Find(const std::string &name) const
const Range_t & Leaves() const noexcept
static const std::vector< std::string > & ParamNames()
TMatrixTBase< Element > & ResizeTo(const TMatrixT< Element > &m)
const char * Data() const
Bool_t IsNull() const
TVectorT< Element > & ResizeTo(const TVectorT< Element > &v)
std::vector< NodeValues > composites
std::vector< NodeValues > leaves
Bool_t IsComposite() const noexcept
std::vector< ParamRef > params
const char * prmName