Project
Loading...
Searching...
No Matches
AlignmentHierarchy.cxx
Go to the documentation of this file.
1// Copyright 2019-2026 CERN and copyright holders of ALICE O2.
2// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders.
3// All rights not expressly granted are reserved.
4//
5// This software is distributed under the terms of the GNU General Public
6// License v3 (GPL Version 3), copied verbatim in the file "COPYING".
7//
8// In applying this license CERN does not waive the privileges and immunities
9// granted to it by virtue of its status as an Intergovernmental Organization
10// or submit itself to any jurisdiction.
11
12#include <format>
13#include <fstream>
14#include <sstream>
15#include <fnmatch.h>
16#include <cmath>
17#include <TGeoManager.h>
18#include <TGeoPhysicalNode.h>
19#include <nlohmann/json.hpp>
20
23#include "Framework/Logger.h"
24#include "MathUtils/Utils.h"
25
26namespace o2::its3::align
27{
28
29void HierarchyConstraint::write(std::ostream& os) const
30{
31 os << "!!! " << mName << '\n';
32 os << "Constraint " << mValue << '\n';
33 for (size_t i{0}; i < mLabels.size(); ++i) {
34 os << mLabels[i] << " " << mCoeff[i] << '\n';
35 }
36 os << '\n';
37}
38
39AlignableVolume::AlignableVolume(const char* symName, uint32_t label, uint32_t det, bool sens) : mSymName(symName), mLabel(det, label, sens)
40{
41 init();
42}
43
44AlignableVolume::AlignableVolume(const char* symName, GlobalLabel label) : mSymName(symName), mLabel(label)
45{
46 init();
47}
48
49void AlignableVolume::init()
50{
51 // check if this sym volume actually exists
52 mPNE = gGeoManager->GetAlignableEntry(mSymName.c_str());
53 if (mPNE == nullptr) {
54 LOGP(fatal, "Symbolic volume '{}' has no corresponding alignable entry!", mSymName);
55 }
56 mPN = mPNE->GetPhysicalNode();
57 if (mPN == nullptr) {
58 LOGP(debug, "Adding physical node to {}", mSymName);
59 mPN = gGeoManager->MakePhysicalNode(mPNE->GetTitle());
60 if (mPN == nullptr) {
61 LOGP(fatal, "Failed to make physical node for {}", mSymName);
62 }
63 }
64}
65
67{
68 if (level == 0 && !isRoot()) {
69 LOGP(fatal, "Finalise should be called only from the root node!");
70 }
71 mLevel = level;
72 if (!isLeaf()) {
73 // depth first
74 for (const auto& c : mChildren) {
75 c->finalise(level + 1);
76 }
77 // auto-disable parent RB DOFs if no children are active
78 if (mRigidBody) {
79 int nActiveChildren = 0;
80 for (const auto& c : mChildren) {
81 if (c->isActive()) {
82 ++nActiveChildren;
83 }
84 }
85 if (!nActiveChildren) {
86 for (int iDOF = 0; iDOF < mRigidBody->nDOFs(); ++iDOF) {
87 if (mRigidBody->isFree(iDOF)) {
88 LOGP(warn, "Auto-disabling DOF {} for {} since no active children",
89 mRigidBody->dofName(iDOF), mSymName);
90 mRigidBody->setFree(iDOF, false);
91 }
92 }
93 }
94 }
95 } else {
96 // for sensors we need also to define the transformation from the measurment (TRK) to the local frame (LOC)
97 // need to it with including possible pre-alignment to allow for iterative convergence
98 // (TRK) is defined wrt global z-axis
101 }
102 if (!isRoot()) {
103 // prepare the transformation matrices, e.g. from child frame to parent frame
104 // this is not necessarily just one level transformation
105 TGeoHMatrix mat = *mPN->GetMatrix(); // global matrix (including possible pre-alignment) from this volume to the global frame
106 if (isLeaf()) {
107 mat = mL2G; // for sensor volumes they might have redefined the L2G definition
108 }
109 auto inv = mParent->mPN->GetMatrix()->Inverse(); // global (including possible pre-alignment) from this volume to the global frame
110 mat.MultiplyLeft(inv); // left mult. effectively subtracts the parent transformation which is included in the the childs
111 mL2P = mat; // now this is directly the child to the parent transformation (LOC) (including possible pre-alignment)
112
113 // prepare jacobian from child to parent frame
114 Eigen::Map<const Eigen::Matrix<double, 3, 3, Eigen::RowMajor>> rotL2P(mL2P.GetRotationMatrix());
115 Eigen::Matrix3d rotInv = rotL2P.transpose(); // parent-to-child rotation
116 const double* t = mL2P.GetTranslation(); // child origin in parent frame
117 Eigen::Matrix3d skewT;
118 skewT << 0, -t[2], t[1], t[2], 0, -t[0], -t[1], t[0], 0;
119 mJL2P.setZero();
120 mJL2P.topLeftCorner<3, 3>() = rotInv;
121 mJL2P.topRightCorner<3, 3>() = -rotInv * skewT;
122 mJL2P.bottomRightCorner<3, 3>() = rotInv;
123 mJP2L = mJL2P.inverse();
124 }
125}
126
128{
129 if (isLeaf() || !mRigidBody) {
130 // recurse even if this node has no RB DOFs
131 for (const auto& c : mChildren) {
132 c->writeRigidBodyConstraints(os);
133 }
134 return;
135 }
136
137 for (int iDOF = 0; iDOF < mRigidBody->nDOFs(); ++iDOF) {
138 if (!mRigidBody->isFree(iDOF)) {
139 continue;
140 }
141 double nActiveChildren = 0.;
142 for (const auto& c : mChildren) {
143 if (c->isActive()) {
144 ++nActiveChildren;
145 }
146 }
147 if (nActiveChildren == 0.) {
148 LOGP(fatal, "{} has dof {} active but no active children!", mSymName, mRigidBody->dofName(iDOF));
149 }
150 const double invN = 1.0 / nActiveChildren;
151 HierarchyConstraint con(std::format("DOF {} for {}", mRigidBody->dofName(iDOF), mSymName), 0.0);
152 for (const auto& c : mChildren) {
153 if (!c->mRigidBody) {
154 continue;
155 }
156 for (int jDOF = 0; jDOF < c->mRigidBody->nDOFs(); ++jDOF) {
157 if (!c->mRigidBody->isFree(jDOF)) {
158 continue;
159 }
160 double coeff = invN * c->getJP2L()(iDOF, jDOF);
161 if (std::abs(coeff) > 1e-16f) {
162 con.add(c->getLabel().raw(jDOF), coeff);
163 }
164 }
165 }
166
167 if (con.getSize() > 1) {
168 con.write(os);
169 }
170 }
171 for (const auto& c : mChildren) {
172 c->writeRigidBodyConstraints(os);
173 }
174}
175
176void AlignableVolume::writeParameters(std::ostream& os) const
177{
178 if (isRoot()) {
179 os << "Parameter\n";
180 }
181 if (!mIsPseudo) {
182 if (mRigidBody) {
183 for (int iDOF = 0; iDOF < mRigidBody->nDOFs(); ++iDOF) {
184 os << std::format("{:<10} {:>+15g} {:>+15g} ! {} {} ",
185 mLabel.raw(iDOF), 0.0, (mRigidBody->isFree(iDOF) ? 0.0 : -1.0),
186 (mRigidBody->isFree(iDOF) ? 'V' : 'F'), mRigidBody->dofName(iDOF))
187 << mSymName << '\n';
188 }
189 }
190 if (mCalib) {
191 auto calibLbl = mLabel.asCalib();
192 for (int iDOF = 0; iDOF < mCalib->nDOFs(); ++iDOF) {
193 os << std::format("{:<10} {:>+15g} {:>+15g} ! {} {:<5} ",
194 calibLbl.raw(iDOF), 0.0, (mCalib->isFree(iDOF) ? 0.0 : -1.0),
195 (mCalib->isFree(iDOF) ? 'V' : 'F'), mCalib->dofName(iDOF))
196 << mSymName << '\n';
197 }
198 }
199 }
200 for (const auto& c : mChildren) {
201 c->writeParameters(os);
202 }
203}
204
205void AlignableVolume::writeTree(std::ostream& os, int indent) const
206{
207 os << std::string(static_cast<size_t>(indent * 2), ' ') << mSymName << (mLabel.sens() ? " (sens)" : " (pasv)");
208 if (mIsPseudo) {
209 os << " pseudo";
210 } else {
211 int nFreeDofs{0};
212 if (mRigidBody && mRigidBody->nFreeDOFs()) {
213 nFreeDofs += mRigidBody->nFreeDOFs();
214 os << " RB[";
215 for (int i = 0; i < mRigidBody->nDOFs(); ++i) {
216 if (mRigidBody->isFree(i)) {
217 os << " " << mRigidBody->dofName(i) << "(" << mLabel.raw(i) << ")";
218 }
219 }
220 os << " ]";
221 }
222 if (mCalib && mCalib->nFreeDOFs()) {
223 nFreeDofs += mCalib->nFreeDOFs();
224 os << " CAL[";
225 auto calibLbl = mLabel.asCalib();
226 for (int i = 0; i < mCalib->nDOFs(); ++i) {
227 if (mCalib->isFree(i)) {
228 os << " " << mCalib->dofName(i) << "(" << calibLbl.raw(i) << ")";
229 }
230 }
231 os << " ]";
232 }
233 if (!nFreeDofs) {
234 os << " no DOFs";
235 }
236 }
237 os << '\n';
238 for (const auto& c : mChildren) {
239 c->writeTree(os, indent + 2);
240 }
241}
242
243void applyDOFConfig(AlignableVolume* root, const std::string& jsonPath)
244{
245 using json = nlohmann::json;
246 std::ifstream f(jsonPath);
247 if (!f.is_open()) {
248 LOGP(fatal, "Cannot open DOF config file: {}", jsonPath);
249 }
250 auto data = json::parse(f);
251 json rules = data.is_array() ? data : data.value("rules", json::array());
252
253 static const std::map<std::string, int> rbNameToIdx = {
254 {"TX", 0}, {"TY", 1}, {"TZ", 2}, {"RX", 3}, {"RY", 4}, {"RZ", 5}};
255
256 auto matchPattern = [](const std::string& pattern, const std::string& sym) -> bool {
257 if (fnmatch(pattern.c_str(), sym.c_str(), 0) == 0) {
258 return true;
259 }
260 std::string prefixed = "*" + pattern;
261 return fnmatch(prefixed.c_str(), sym.c_str(), 0) == 0;
262 };
263
264 if (data.is_object() && data.contains("defaults")) {
265 json defRule = data["defaults"];
266 defRule["match"] = "*";
267 rules.insert(rules.begin(), defRule);
268 }
269
270 root->traverse([&](AlignableVolume* vol) {
271 if (vol->isPseudo()) {
272 return;
273 }
274 const std::string& sym = vol->getSymName();
275 for (const auto& rule : rules) {
276 const auto pattern = rule["match"].get<std::string>();
277 if (!matchPattern(pattern, sym)) {
278 continue;
279 }
280 // rigid body DOFs
281 if (rule.contains("rigidBody")) {
282 const auto& rb = rule["rigidBody"];
283 if (rb.is_string()) {
284 auto s = rb.get<std::string>();
285 if (s == "all" || s == "free") {
286 vol->setRigidBody(std::make_unique<RigidBodyDOFSet>());
287 } else if (s == "fixed") {
288 auto dofSet = std::make_unique<RigidBodyDOFSet>();
289 dofSet->setAllFree(false);
290 vol->setRigidBody(std::move(dofSet));
291 }
292 } else if (rb.is_array()) {
293 auto dofSet = std::make_unique<RigidBodyDOFSet>();
294 dofSet->setAllFree(false);
295 for (const auto& name : rb) {
296 auto it = rbNameToIdx.find(name.get<std::string>());
297 if (it != rbNameToIdx.end()) {
298 dofSet->setFree(it->second, true);
299 }
300 }
301 vol->setRigidBody(std::move(dofSet));
302 } else if (rb.is_object()) {
303 auto dofs = rb.value("dofs", std::string("all"));
304 bool fixed = rb.value("fixed", false);
305 if (dofs == "all") {
306 auto dofSet = std::make_unique<RigidBodyDOFSet>();
307 if (fixed) {
308 dofSet->setAllFree(false);
309 }
310 vol->setRigidBody(std::move(dofSet));
311 } else if (rb["dofs"].is_array()) {
312 auto dofSet = std::make_unique<RigidBodyDOFSet>();
313 dofSet->setAllFree(false);
314 for (const auto& name : rb["dofs"]) {
315 auto it = rbNameToIdx.find(name.get<std::string>());
316 if (it != rbNameToIdx.end()) {
317 dofSet->setFree(it->second, !fixed);
318 }
319 }
320 vol->setRigidBody(std::move(dofSet));
321 }
322 }
323 }
324 // calibration DOFs
325 if (rule.contains("calib")) {
326 const auto& cal = rule["calib"];
327 auto calType = cal.value("type", std::string(""));
328 if (calType == "legendre") {
329 int order = cal.value("order", 3);
330 auto dofSet = std::make_unique<LegendreDOFSet>(order);
331 bool fixed = cal.value("fixed", false);
332 if (fixed) {
333 dofSet->setAllFree(false);
334 }
335 // fix/free individual coefficients by name or index
336 if (cal.contains("free")) {
337 dofSet->setAllFree(false);
338 for (const auto& item : cal["free"]) {
339 if (item.is_number_integer()) {
340 dofSet->setFree(item.get<int>(), true);
341 } else if (item.is_string()) {
342 // match by name e.g. "L(1,0)"
343 for (int k = 0; k < dofSet->nDOFs(); ++k) {
344 if (dofSet->dofName(k) == item.get<std::string>()) {
345 dofSet->setFree(k, true);
346 }
347 }
348 }
349 }
350 }
351 if (cal.contains("fix")) {
352 for (const auto& item : cal["fix"]) {
353 if (item.is_number_integer()) {
354 dofSet->setFree(item.get<int>(), false);
355 } else if (item.is_string()) {
356 for (int k = 0; k < dofSet->nDOFs(); ++k) {
357 if (dofSet->dofName(k) == item.get<std::string>()) {
358 dofSet->setFree(k, false);
359 }
360 }
361 }
362 }
363 }
364 vol->setCalib(std::move(dofSet));
365 } else if (calType == "inextensional") {
366 int maxOrder = cal.value("order", 2);
367 auto dofSet = std::make_unique<InextensionalDOFSet>(maxOrder);
368 bool fixed = cal.value("fixed", false);
369 if (fixed) {
370 dofSet->setAllFree(false);
371 }
372 if (cal.contains("free")) {
373 dofSet->setAllFree(false);
374 for (const auto& item : cal["free"]) {
375 if (item.is_number_integer()) {
376 dofSet->setFree(item.get<int>(), true);
377 } else if (item.is_string()) {
378 for (int k = 0; k < dofSet->nDOFs(); ++k) {
379 if (dofSet->dofName(k) == item.get<std::string>()) {
380 dofSet->setFree(k, true);
381 }
382 }
383 }
384 }
385 }
386 if (cal.contains("fix")) {
387 for (const auto& item : cal["fix"]) {
388 if (item.is_number_integer()) {
389 dofSet->setFree(item.get<int>(), false);
390 } else if (item.is_string()) {
391 for (int k = 0; k < dofSet->nDOFs(); ++k) {
392 if (dofSet->dofName(k) == item.get<std::string>()) {
393 dofSet->setFree(k, false);
394 }
395 }
396 }
397 }
398 }
399 vol->setCalib(std::move(dofSet));
400 }
401 }
402 }
403 });
404}
405
406void writeMillepedeResults(AlignableVolume* root, const std::string& milleResPath, const std::string& outJsonPath, const std::string& injectedJsonPath)
407{
408 using json = nlohmann::json;
409
410 // parse millepede.res: label fittedValue presigma [...]
411 std::ifstream fin(milleResPath);
412 if (!fin.is_open()) {
413 LOGP(fatal, "Cannot open millepede result file: {}", milleResPath);
414 }
415 std::map<uint32_t, double> labelToValue;
416 std::string line;
417 while (std::getline(fin, line)) {
418 if (line.empty() || line[0] == '!' || line[0] == '*') {
419 continue;
420 }
421 if (line.find("Parameter") != std::string::npos) {
422 continue;
423 }
424 std::istringstream iss(line);
425 uint32_t label = 0;
426 double value = NAN, presigma = NAN;
427 if (!(iss >> label >> value >> presigma)) {
428 continue;
429 }
430 if (presigma >= 0.0) { // skip fixed parameters
431 labelToValue[label] = value;
432 }
433 }
434 fin.close();
435 LOGP(info, "Parsed {} not fixed parameters from {}", labelToValue.size(), milleResPath);
436
437 // load injected misalignment if provided (same format as closure test input)
438 // indexed by sensorID
439 std::map<int, std::vector<double>> injRB;
440 std::map<int, std::vector<std::vector<double>>> injMatrix;
441 struct InjInex {
442 std::map<int, std::array<double, 4>> modes;
443 double alpha{0.};
444 double beta{0.};
445 };
446 std::map<int, InjInex> injInex;
447 if (!injectedJsonPath.empty()) {
448 std::ifstream injFile(injectedJsonPath);
449 if (injFile.is_open()) {
450 json injData = json::parse(injFile);
451 for (const auto& item : injData) {
452 int id = item["id"].get<int>();
453 if (item.contains("rigidBody")) {
454 injRB[id] = item["rigidBody"].get<std::vector<double>>();
455 }
456 if (item.contains("matrix")) {
457 injMatrix[id] = item["matrix"].get<std::vector<std::vector<double>>>();
458 }
459 if (item.contains("inextensional")) {
460 InjInex ii;
461 const auto& inex = item["inextensional"];
462 if (inex.contains("modes")) {
463 for (auto& [key, val] : inex["modes"].items()) {
464 ii.modes[std::stoi(key)] = val.get<std::array<double, 4>>();
465 }
466 }
467 if (inex.contains("alpha")) {
468 ii.alpha = inex["alpha"].get<double>();
469 }
470 if (inex.contains("beta")) {
471 ii.beta = inex["beta"].get<double>();
472 }
473 injInex[id] = ii;
474 }
475 }
476 LOGP(info, "Loaded injected misalignment for {} sensors", injData.size());
477 } else {
478 LOGP(warn, "Cannot open injected misalignment file: {}, writing absolute values", injectedJsonPath);
479 }
480 }
481
482 // collect results per volume that has RB or calib DOFs
483 json output = json::array();
484 root->traverse([&](AlignableVolume* vol) {
485 auto* rb = vol->getRigidBody();
486 auto* cal = vol->getCalib();
487 if ((!rb && !cal) || vol->isPseudo()) {
488 return;
489 }
490 int id = vol->getSensorId();
491 json entry;
492 entry["symName"] = vol->getSymName();
493 entry["id"] = id;
494 bool write = false;
495
496 // rigid body parameters
497 if (rb && rb->nFreeDOFs()) {
498 write = true;
499 json rbArr = json::array();
500 const auto& inj = injRB.contains(id) ? injRB[id] : std::vector<double>{};
501 for (int i = 0; i < rb->nDOFs(); ++i) {
502 uint32_t raw = vol->getLabel().raw(i);
503 auto it = labelToValue.find(raw);
504 double fitted = it != labelToValue.end() ? it->second : 0.0;
505 double ref = i < static_cast<int>(inj.size()) ? inj[i] : 0.0;
506 rbArr.push_back(fitted - ref);
507 }
508 entry["rigidBody"] = rbArr;
509 }
510
511 // calibration (Legendre) parameters
512 if (cal && cal->nFreeDOFs() && cal->type() == DOFSet::Type::Legendre) {
513 write = true;
514 auto* leg = dynamic_cast<const LegendreDOFSet*>(cal);
515 int order = leg->order();
516 auto calibLbl = vol->getLabel().asCalib();
517 const auto& inj = injMatrix.contains(id) ? injMatrix[id] : std::vector<std::vector<double>>{};
518 json matrix = json::array();
519 int idx = 0;
520 for (int i = 0; i <= order; ++i) {
521 json row = json::array();
522 for (int j = 0; j <= i; ++j) {
523 uint32_t raw = calibLbl.raw(idx);
524 auto it = labelToValue.find(raw);
525 double fitted = it != labelToValue.end() ? it->second : 0.0;
526 double ref = (i < static_cast<int>(inj.size()) && j < static_cast<int>(inj[i].size())) ? inj[i][j] : 0.0;
527 row.push_back(fitted - ref);
528 ++idx;
529 }
530 matrix.push_back(row);
531 }
532 entry["matrix"] = matrix;
533 } else if (cal && cal->nFreeDOFs() && cal->type() == DOFSet::Type::Inextensional) {
534 write = true;
535 auto* inexSet = static_cast<const InextensionalDOFSet*>(cal);
536 int maxN = inexSet->maxOrder();
537 auto calibLbl = vol->getLabel().asCalib();
538 const auto& inj = injInex.contains(id) ? injInex[id] : InjInex{};
539
540 json inexEntry;
541 json modesObj = json::object();
542 for (int n = 2; n <= maxN; ++n) {
544 std::array<double, 4> injCoeffs = {0., 0., 0., 0.};
545 if (inj.modes.contains(n)) {
546 injCoeffs = inj.modes.at(n);
547 }
548 json modeArr = json::array();
549 for (int k = 0; k < 4; ++k) {
550 uint32_t raw = calibLbl.raw(off + k);
551 auto it = labelToValue.find(raw);
552 double fitted = it != labelToValue.end() ? it->second : 0.0;
553 modeArr.push_back(fitted - injCoeffs[k]);
554 }
555 modesObj[std::to_string(n)] = modeArr;
556 }
557 inexEntry["modes"] = modesObj;
558
559 // alpha
560 uint32_t rawAlpha = calibLbl.raw(inexSet->alphaIdx());
561 auto itA = labelToValue.find(rawAlpha);
562 inexEntry["alpha"] = (itA != labelToValue.end() ? itA->second : 0.0) - inj.alpha;
563
564 // beta
565 uint32_t rawBeta = calibLbl.raw(inexSet->betaIdx());
566 auto itB = labelToValue.find(rawBeta);
567 inexEntry["beta"] = (itB != labelToValue.end() ? itB->second : 0.0) - inj.beta;
568
569 entry["inextensional"] = inexEntry;
570 }
571 if (write) {
572 output.push_back(entry);
573 }
574 });
575
576 std::ofstream fout(outJsonPath);
577 if (!fout.is_open()) {
578 LOGP(fatal, "Cannot open output file: {}", outJsonPath);
579 }
580 fout << output.dump(2) << '\n';
581 fout.close();
582 LOGP(info, "Wrote millepede results to {}", outJsonPath);
583}
584
585} // namespace o2::its3::align
General auxilliary methods.
std::ostringstream debug
int32_t i
Definition of the GeometryTGeo class.
void output(const std::map< std::string, ChannelStat > &channels)
Definition rawdump.cxx:197
uint32_t j
Definition RawData.h:0
uint32_t c
Definition RawData.h:2
nlohmann::json json
StringRef key
constexpr T raw(T dof) const noexcept
produce the raw Millepede label for a given DOF index (rigid body: calib=0 in label)
GlobalLabel asCalib() const noexcept
return a copy of this label with the CALIB bit set (for calibration DOFs on same volume)
constexpr bool sens() const noexcept
static int modeOffset(int n)
int order() const
void traverse(const std::function< void(AlignableVolume *)> &visitor)
void writeParameters(std::ostream &os) const
GlobalLabel getLabel() const noexcept
AlignableVolume * mParent
matrices
void setRigidBody(std::unique_ptr< DOFSet > rb)
void writeTree(std::ostream &os, int indent=0) const
std::string getSymName() const noexcept
void writeRigidBodyConstraints(std::ostream &os) const
AlignableVolume(const AlignableVolume &)=delete
void setCalib(std::unique_ptr< DOFSet > cal)
void add(uint32_t lab, double coeff)
GLdouble n
Definition glcorearb.h:1982
GLfloat GLfloat GLfloat alpha
Definition glcorearb.h:279
GLuint entry
Definition glcorearb.h:5735
GLsizeiptr size
Definition glcorearb.h:659
GLuint const GLchar * name
Definition glcorearb.h:781
GLdouble f
Definition glcorearb.h:310
GLsizei const GLfloat * value
Definition glcorearb.h:819
GLboolean * data
Definition glcorearb.h:298
GLuint GLsizei const GLchar * label
Definition glcorearb.h:2519
GLuint GLfloat * val
Definition glcorearb.h:1582
GLint level
Definition glcorearb.h:275
GLuint id
Definition glcorearb.h:650
void writeMillepedeResults(AlignableVolume *root, const std::string &milleResPath, const std::string &outJsonPath, const std::string &injectedJsonPath="")
void applyDOFConfig(AlignableVolume *root, const std::string &jsonPath)
std::string to_string(gsl::span< T, Size > span)
Definition common.h:52
std::vector< int > row
std::array< uint16_t, 5 > pattern