Project
Loading...
Searching...
No Matches
o2sim_geometry_doctor.cxx
Go to the documentation of this file.
1// Copyright 2019-2020 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
37
38#include "Field/MagneticField.h"
39
40#include <TFile.h>
41#include <TGeoBBox.h>
42#include <TGeoCone.h>
43#include <TGeoEltu.h>
44#include <TGeoManager.h>
45#include <TGeoMatrix.h>
46#include <TGeoMedium.h>
47#include <TGeoNode.h>
48#include <TGeoPcon.h>
49#include <TGeoPgon.h>
50#include <TGeoShape.h>
51#include <TGeoTube.h>
52#include <TGeoVolume.h>
53#include <TRandom3.h>
54#include <TVectorD.h>
55
56#include <boost/program_options.hpp>
57#include <nlohmann/json.hpp>
58
59#include <algorithm>
60#include <cmath>
61#include <cstdio>
62#include <cstring>
63#include <ctime>
64#include <fstream>
65#include <iostream>
66#include <map>
67#include <set>
68#include <string>
69#include <utility>
70#include <vector>
71
72namespace bpo = boost::program_options;
73using json = nlohmann::json;
74
75namespace
76{
77
78// ---------------------------------------------------------------------------
79// small helpers
80// ---------------------------------------------------------------------------
81
83template <typename... Args>
84std::string form(const char* fmt, Args... args)
85{
86 char buffer[4096];
87 std::snprintf(buffer, sizeof(buffer), fmt, args...);
88 return std::string(buffer);
89}
90
92class Report
93{
94 public:
95 void operator()(const std::string& line)
96 {
97 std::cout << line << '\n';
98 mLines.push_back(line);
99 }
100 void write(const std::string& path) const
101 {
102 std::ofstream out(path);
103 for (const auto& line : mLines) {
104 out << line << '\n';
105 }
106 }
107
108 private:
109 std::vector<std::string> mLines;
110};
111
113void progress(const std::string& line) { std::cerr << line << std::endl; }
114
115// ---------------------------------------------------------------------------
116// the field
117// ---------------------------------------------------------------------------
118
119// A serialized MagneticField keeps its measured map in a transient member, so a
120// reader has to call CreateField() again to get a usable field back. That call
121// carries a trap: it feeds mMultipicativeFactorSolenoid/Dipole back through
122// setters which negate under the LHC polarity convention, so CreateField() is not
123// idempotent and the second call inverts the polarity of both the measured map
124// and the machine compensators. |B| is untouched, which is exactly why the flip
125// survives any magnitude-based check -- a 200k-point round trip on |B| passes
126// while every field vector points the wrong way.
127//
128// A field file therefore has to carry reference field VECTORS taken from the live
129// object at write time. The loader re-evaluates them, repairs a pure global flip,
130// and refuses the file if what it gets back is anything other than what was
131// written. A file without those probes cannot be verified and is refused too;
132// --field-current builds the field from scratch instead.
133
134constexpr const char* kFieldObjectKey = "MagneticField";
135constexpr const char* kFieldProbeKey = "ReferenceProbes";
136
138int compareToProbes(o2::field::MagneticField* field, const TVectorD& probes)
139{
140 const int n = probes.GetNrows() / 6;
141 bool same = true;
142 bool flipped = true;
143 for (int i = 0; i < n; ++i) {
144 double x[3] = {probes[6 * i], probes[6 * i + 1], probes[6 * i + 2]};
145 double b[3] = {0., 0., 0.};
146 field->Field(x, b);
147 for (int k = 0; k < 3; ++k) {
148 const double want = probes[6 * i + 3 + k];
149 same = same && (b[k] == want);
150 flipped = flipped && (b[k] == -want);
151 }
152 }
153 return same ? 0 : (flipped ? 1 : -1);
154}
155
156o2::field::MagneticField* loadFieldFromFile(const std::string& path)
157{
158 TFile* file = TFile::Open(path.c_str());
159 if (file == nullptr || file->IsZombie()) {
160 progress("error: cannot open field file " + path);
161 return nullptr;
162 }
163 auto* field = dynamic_cast<o2::field::MagneticField*>(file->Get(kFieldObjectKey));
164 auto* probes = dynamic_cast<TVectorD*>(file->Get(kFieldProbeKey));
165 if (field == nullptr) {
166 progress(form("error: no '%s' object in %s", kFieldObjectKey, path.c_str()));
167 return nullptr;
168 }
169 if (probes == nullptr) {
170 progress(form(
171 "error: no '%s' in %s -- the field cannot be verified against what was written, "
172 "and a silently inverted field is exactly what this check exists to catch. "
173 "Use --field-current instead.",
174 kFieldProbeKey, path.c_str()));
175 return nullptr;
176 }
177 const TVectorD reference(*probes);
178 file->Close();
179 delete file;
180
181 // Reload the parameterisation from this file, whatever path was stored at write
182 // time, so that the file is self-contained and relocatable.
183 field->setDataFileName(path.c_str());
184 field->CreateField();
185
186 int comparison = compareToProbes(field, reference);
187 if (comparison == 1) {
188 field->setFactorSolenoid(-field->getFactorSolenoid());
189 field->setFactorDipole(-field->getFactorDipole());
190 comparison = compareToProbes(field, reference);
191 if (comparison == 0) {
192 progress("field: polarity flip from the non-idempotent CreateField() detected and repaired");
193 }
194 }
195 if (comparison != 0) {
196 progress(form(
197 "error: the field reloaded from %s does not reproduce its own reference probes; "
198 "refusing to hand back a field that is not the one written",
199 path.c_str()));
200 return nullptr;
201 }
202 progress(form("field: %s verified against %d reference probe vectors", path.c_str(), reference.GetNrows() / 6));
203 return field;
204}
205
206double fieldMag(o2::field::MagneticField* field, double x, double y, double z)
207{
208 double point[3] = {x, y, z};
209 double b[3] = {0., 0., 0.};
210 field->Field(point, b);
211 return std::sqrt(b[0] * b[0] + b[1] * b[1] + b[2] * b[2]);
212}
213
214double fieldMagCyl(o2::field::MagneticField* field, double r, double phi, double z)
215{
216 return fieldMag(field, r * std::cos(phi), r * std::sin(phi), z);
217}
218
219// ---------------------------------------------------------------------------
220// the field-support model
221// ---------------------------------------------------------------------------
222
223// The model states, per threshold, a list of z-bands each carrying the radial
224// intervals in which |B| exceeds that threshold, maximised over phi. It is an
225// OUTER bound: a point outside every band has |B| <= threshold. That direction is
226// what lets a sampled quantity support a geometric argument about a volume.
227//
228// Two features of the real field dictate the sampling, and both were found by
229// this model getting them wrong first:
230//
231// * the LHC machine elements are hard cylinders with a discontinuous edge (the
232// A-side compensator aperture is exactly r < 4.0 cm), so every threshold
233// crossing is bisected rather than left on the grid;
234//
235// * the measured map's coverage is a BOX in (x, y), not a cylinder. Between the
236// box's inscribed and corner radius the field survives only in ~2 degree wedges
237// at the four corners -- at r = 194.5, z = -797.9 it is 8.2 kG at phi = 132.8
238// degrees and exactly zero at 127.5 and 135. A model sampling 16 phi values
239// steps straight over those wedges and declares 8 kG of dipole field
240// unsupported. phi sampling is therefore bounded by ARC LENGTH, so the angular
241// resolution follows the feature size at every radius.
242//
243// Interval edges are stored as best estimates with a separate edge uncertainty
244// rather than pre-inflated, because the study this tool comes from turns on a
245// volume whose inner radius is exactly the field boundary: it is separated from
246// the field by exactly zero, and no amount of inflation may promote that to a
247// margin.
248
249constexpr int kMinPhiSamples = 24;
250constexpr double kPhiArcStep = 3.0; // cm, the azimuthal sampling bound
251constexpr double kBisectionTol = 0.01; // cm, also the model's edge uncertainty
252constexpr double kZStepCoarse = 1.0; // cm
253constexpr double kZStepRefine = 0.1; // cm, used wherever the radial structure changes
254constexpr double kScanRMax = 2100.; // cm, outer reach of the scan
255constexpr double kScanRMaxFine = 900.; // cm, beyond this the radial grid is coarse
256constexpr double kTightMargin = 0.05; // cm, for boundaries that are analytically hard
257constexpr long kViolationScanPoints = 400000;
258
259struct Interval {
260 double lo, hi;
261};
262
263struct Band {
264 double zlo, zhi;
265 std::vector<Interval> iv;
266};
267
268struct Model {
269 double thresholdKG = 0.;
270 std::vector<Band> bands;
271};
272
273struct Support {
274 std::vector<Model> models;
275 double edgeUncertainty = kBisectionTol;
276 double marginStrict = 5.0;
277 double marginTight = kTightMargin;
278 double zmin = 0., zmax = 0., rmax = 0.;
279 std::string parameterisation;
280
285 double separation(int t, double vzmin, double vzmax, double vrmin, double vrmax) const
286 {
287 double best = 1e30;
288 for (const auto& band : models[t].bands) {
289 const double dz = std::max(0., std::max(band.zlo - vzmax, vzmin - band.zhi));
290 for (const auto& iv : band.iv) {
291 const double lo = iv.lo - edgeUncertainty;
292 const double hi = iv.hi + edgeUncertainty;
293 const double dr = std::max(0., std::max(lo - vrmax, vrmin - hi));
294 best = std::min(best, std::sqrt(dz * dz + dr * dr));
295 if (best <= 0.) {
296 return 0.;
297 }
298 }
299 }
300 return (best > 1e29) ? 1e30 : best;
301 }
302
303 bool supportAt(int t, double z, double r) const { return separation(t, z, z, r, r) <= 0.; }
304
309 double penetration(int t, double vzmin, double vzmax, double vrmin, double vrmax) const
310 {
311 double worst = 0.;
312 for (const auto& band : models[t].bands) {
313 if (band.zlo > vzmax || band.zhi < vzmin) {
314 continue;
315 }
316 for (const auto& iv : band.iv) {
317 const double lo = iv.lo - edgeUncertainty;
318 const double hi = iv.hi + edgeUncertainty;
319 if (lo > vrmax || hi < vrmin) {
320 continue;
321 }
322 worst = std::max(worst, std::min(hi - vrmin, vrmax - lo));
323 }
324 }
325 return worst;
326 }
327
335 bool coveredBySupport(int t, double vzmin, double vzmax, double vrmin, double vrmax) const
336 {
337 std::vector<std::pair<double, double>> covering;
338 for (const auto& band : models[t].bands) {
339 if (band.zhi < vzmin || band.zlo > vzmax) {
340 continue;
341 }
342 for (const auto& iv : band.iv) {
343 if (iv.lo - edgeUncertainty <= vrmin && iv.hi + edgeUncertainty >= vrmax) {
344 covering.push_back({band.zlo, band.zhi});
345 break;
346 }
347 }
348 }
349 std::sort(covering.begin(), covering.end());
350 double frontier = vzmin;
351 for (const auto& segment : covering) {
352 if (segment.first > frontier + 1e-9) {
353 return false;
354 }
355 frontier = std::max(frontier, segment.second);
356 if (frontier >= vzmax) {
357 return true;
358 }
359 }
360 return frontier >= vzmax;
361 }
362
364 bool inDomain(double vzmin, double vzmax, double vrmax) const
365 {
366 return vzmin >= zmin && vzmax <= zmax && vrmax <= rmax;
367 }
368};
369
370int phiSamplesAt(double r)
371{
372 if (r <= 0.) {
373 return 1;
374 }
375 return std::max(kMinPhiSamples, (int)std::ceil(2 * M_PI * r / kPhiArcStep));
376}
377
378double maxFieldOverPhi(o2::field::MagneticField* field, double r, double z)
379{
380 if (r == 0.) {
381 return fieldMag(field, 0., 0., z);
382 }
383 const int n = phiSamplesAt(r);
384 double worst = 0.;
385 for (int i = 0; i < n; ++i) {
386 worst = std::max(worst, fieldMagCyl(field, r, 2 * M_PI * i / n, z));
387 }
388 return worst;
389}
390
391std::vector<double> radialGrid()
392{
393 std::vector<double> grid;
394 for (double r = 0.; r < 20.; r += 0.1) {
395 grid.push_back(r);
396 }
397 for (double r = 20.; r < 100.; r += 1.0) {
398 grid.push_back(r);
399 }
400 for (double r = 100.; r < 800.; r += 2.0) {
401 grid.push_back(r);
402 }
403 for (double r = 800.; r <= kScanRMaxFine; r += 10.0) {
404 grid.push_back(r);
405 }
406 // A coarse extension so that an unexpected far feature is not invisible by construction.
407 for (double r = kScanRMaxFine + 25.; r <= kScanRMax; r += 25.0) {
408 grid.push_back(r);
409 }
410 return grid;
411}
412
417double bisectCrossing(o2::field::MagneticField* field, double z, double rOut, double rIn, double threshold)
418{
419 for (int i = 0; i < 60 && std::fabs(rOut - rIn) > kBisectionTol; ++i) {
420 const double middle = 0.5 * (rOut + rIn);
421 if (maxFieldOverPhi(field, middle, z) > threshold) {
422 rIn = middle;
423 } else {
424 rOut = middle;
425 }
426 }
427 return 0.5 * (rOut + rIn);
428}
429
430struct Slice {
431 double z = 0., zlo = 0., zhi = 0.;
432 std::vector<std::vector<Interval>> iv; // one per threshold
433};
434
435Slice sliceAt(o2::field::MagneticField* field, double z, const std::vector<double>& thresholds)
436{
437 Slice slice;
438 slice.z = z;
439 slice.iv.resize(thresholds.size());
440 const std::vector<double> grid = radialGrid();
441 std::vector<double> b(grid.size());
442 for (size_t i = 0; i < grid.size(); ++i) {
443 b[i] = maxFieldOverPhi(field, grid[i], z);
444 }
445 for (size_t t = 0; t < thresholds.size(); ++t) {
446 bool open = false;
447 Interval current{0., 0.};
448 for (size_t i = 0; i < grid.size(); ++i) {
449 const bool above = b[i] > thresholds[t];
450 if (above && !open) {
451 current.lo = (i > 0) ? std::max(0., bisectCrossing(field, z, grid[i - 1], grid[i], thresholds[t])) : grid[i];
452 open = true;
453 } else if (!above && open) {
454 current.hi = bisectCrossing(field, z, grid[i], grid[i - 1], thresholds[t]);
455 slice.iv[t].push_back(current);
456 open = false;
457 }
458 }
459 if (open) {
460 current.hi = grid.back();
461 slice.iv[t].push_back(current);
462 }
463 }
464 return slice;
465}
466
467void mergeIntervals(std::vector<Interval>& into, const std::vector<Interval>& from)
468{
469 into.insert(into.end(), from.begin(), from.end());
470 if (into.empty()) {
471 return;
472 }
473 std::sort(into.begin(), into.end(), [](const Interval& a, const Interval& b) { return a.lo < b.lo; });
474 std::vector<Interval> merged{into.front()};
475 for (size_t i = 1; i < into.size(); ++i) {
476 if (into[i].lo <= merged.back().hi + 1e-9) {
477 merged.back().hi = std::max(merged.back().hi, into[i].hi);
478 } else {
479 merged.push_back(into[i]);
480 }
481 }
482 into.swap(merged);
483}
484
485bool sameStructure(const std::vector<Interval>& a, const std::vector<Interval>& b)
486{
487 if (a.size() != b.size()) {
488 return false;
489 }
490 for (size_t i = 0; i < a.size(); ++i) {
491 if (std::fabs(a[i].lo - b[i].lo) > 0.02 || std::fabs(a[i].hi - b[i].hi) > 0.02) {
492 return false;
493 }
494 }
495 return true;
496}
497
498Support buildSupport(o2::field::MagneticField* field, const std::vector<double>& thresholds, double zmin, double zmax)
499{
500 progress(form(
501 "support: scanning z %.0f..%.0f, dz %.1f cm refined to %.1f, r to %.0f cm, "
502 "phi by arc length <= %.1f cm (%d samples at r=200)",
503 zmin, zmax, kZStepCoarse, kZStepRefine, kScanRMax, kPhiArcStep, phiSamplesAt(200.)));
504
505 std::vector<Slice> slices;
506 Slice previous;
507 bool havePrevious = false;
508 for (double z = zmin; z <= zmax + 1e-9; z += kZStepCoarse) {
509 Slice slice = sliceAt(field, z, thresholds);
510 bool changed = false;
511 for (size_t t = 0; havePrevious && t < thresholds.size(); ++t) {
512 changed = changed || !sameStructure(previous.iv[t], slice.iv[t]);
513 }
514 if (changed) {
515 for (double zz = previous.z + kZStepRefine; zz < z - 1e-9; zz += kZStepRefine) {
516 slices.push_back(sliceAt(field, zz, thresholds));
517 }
518 }
519 slices.push_back(slice);
520 previous = slice;
521 havePrevious = true;
522 if (std::fmod(z - zmin, 500.) < kZStepCoarse / 2) {
523 progress(form("support: ... z = %.0f", z));
524 }
525 }
526
527 for (size_t i = 0; i < slices.size(); ++i) {
528 const double zPrev = (i == 0) ? slices[i].z - kZStepCoarse : slices[i - 1].z;
529 const double zNext = (i + 1 == slices.size()) ? slices[i].z + kZStepCoarse : slices[i + 1].z;
530 slices[i].zlo = 0.5 * (zPrev + slices[i].z);
531 slices[i].zhi = 0.5 * (slices[i].z + zNext);
532 }
533
534 // A band carries the union of the intervals of ITS OWN slices, extended by half
535 // a coarse step at each end so that neighbouring bands overlap and a point
536 // between two samples is claimed by both. Unioning a band with its bracketing
537 // slices instead is sound only for a band one step wide: applied to a merged
538 // band it smears a neighbour's support across the whole length, and the field-
539 // free windows this tool exists to find are precisely empty bands between two
540 // populated ones. Conservatism has to stay local or it destroys them.
541 Support support;
542 support.models.resize(thresholds.size());
543 for (size_t t = 0; t < thresholds.size(); ++t) {
544 support.models[t].thresholdKG = thresholds[t];
545 size_t i = 0;
546 while (i < slices.size()) {
547 size_t j = i;
548 while (j + 1 < slices.size() && sameStructure(slices[j].iv[t], slices[j + 1].iv[t])) {
549 ++j;
550 }
551 Band band;
552 band.zlo = slices[i].zlo - 0.5 * kZStepCoarse;
553 band.zhi = slices[j].zhi + 0.5 * kZStepCoarse;
554 for (size_t k = i; k <= j; ++k) {
555 mergeIntervals(band.iv, slices[k].iv[t]);
556 }
557 if (!band.iv.empty()) {
558 support.models[t].bands.push_back(band);
559 }
560 i = j + 1;
561 }
562 }
563 support.zmin = zmin;
564 support.zmax = zmax;
565 support.rmax = kScanRMax;
566 support.parameterisation = field->getParameterName();
567 return support;
568}
569
576bool violationScan(o2::field::MagneticField* field, const Support& support, Report& report)
577{
578 TRandom3 random(10001);
579 std::vector<long> violations(support.models.size(), 0);
580 std::vector<double> worst(support.models.size(), 0.);
581 std::vector<double> worstR(support.models.size(), 0.);
582 std::vector<double> worstZ(support.models.size(), 0.);
583 for (long i = 0; i < kViolationScanPoints; ++i) {
584 // Half the points anywhere in the domain, half near the axis where the
585 // machine elements are narrow enough to hide between uniform samples.
586 const bool nearAxis = (i % 2 == 1);
587 const double z = nearAxis ? random.Uniform(std::max(support.zmin, -2200.), std::min(support.zmax, 2200.))
588 : random.Uniform(support.zmin, support.zmax);
589 const double r = random.Uniform(0., nearAxis ? 20. : support.rmax);
590 const double b = fieldMagCyl(field, r, random.Uniform(0., 2 * M_PI), z);
591 for (size_t t = 0; t < support.models.size(); ++t) {
592 if (b > support.models[t].thresholdKG && !support.supportAt(t, z, r)) {
593 ++violations[t];
594 if (b > worst[t]) {
595 worst[t] = b;
596 worstR[t] = r;
597 worstZ[t] = z;
598 }
599 }
600 }
601 }
602 bool ok = true;
603 for (size_t t = 0; t < support.models.size(); ++t) {
604 report(form(" outer bound at %6.1f G: %ld / %ld sampled points with field outside every band%s",
605 support.models[t].thresholdKG * 1000., violations[t], kViolationScanPoints,
606 violations[t] == 0 ? " (bound holds)" : " <-- THE MODEL IS NOT AN OUTER BOUND"));
607 if (violations[t] != 0) {
608 report(form(" worst: |B| = %.4f kG at r = %.3f, z = %.3f", worst[t], worstR[t], worstZ[t]));
609 ok = false;
610 }
611 }
612 return ok;
613}
614
615json supportToJson(const Support& support, const std::string& fieldSource)
616{
617 const std::time_t now = std::time(nullptr);
618 char stamp[64];
619 std::strftime(stamp, sizeof(stamp), "%Y-%m-%dT%H:%M:%S", std::gmtime(&now));
620
621 json out;
622 out["schema"] = "o2-sim-geometry-doctor/field_support/1";
623 out["generated_utc"] = stamp;
624 out["field_source"] = fieldSource;
625 out["parameterisation"] = support.parameterisation;
626 out["units"] = "kGauss, cm";
627 out["semantics"] =
628 "Outer bound on the support of |B|, maximised over phi. A point outside every band, "
629 "after expanding intervals by edge_uncertainty_cm, has |B| <= threshold.";
630 out["resolution"] = {{"dz_coarse", kZStepCoarse},
631 {"dz_refine", kZStepRefine},
632 {"dr_near_axis", 0.1},
633 {"phi_arc_step_cm", kPhiArcStep},
634 {"phi_min_samples", kMinPhiSamples},
635 {"bisection_tol_cm", kBisectionTol},
636 {"edge_uncertainty_cm", support.edgeUncertainty}};
637 out["domain"] = {{"zmin", support.zmin}, {"zmax", support.zmax}, {"rmax", support.rmax}};
638 out["recommended_margins_cm"] = {{"strict", support.marginStrict}, {"tight", support.marginTight}};
639 out["models"] = json::array();
640 for (const auto& model : support.models) {
641 json m;
642 m["threshold_kG"] = model.thresholdKG;
643 m["threshold_gauss"] = model.thresholdKG * 1000.;
644 m["n_bands"] = model.bands.size();
645 m["bands"] = json::array();
646 for (const auto& band : model.bands) {
647 json b;
648 b["zlo"] = band.zlo;
649 b["zhi"] = band.zhi;
650 b["iv"] = json::array();
651 for (const auto& iv : band.iv) {
652 b["iv"].push_back(json::array({iv.lo, iv.hi}));
653 }
654 m["bands"].push_back(b);
655 }
656 out["models"].push_back(m);
657 }
658 return out;
659}
660
663bool supportFromJson(const json& in, Support& support)
664{
665 try {
666 support.edgeUncertainty = in.at("resolution").at("edge_uncertainty_cm").get<double>();
667 support.marginStrict = in.at("recommended_margins_cm").at("strict").get<double>();
668 support.marginTight = in.at("recommended_margins_cm").at("tight").get<double>();
669 support.zmin = in.at("domain").at("zmin").get<double>();
670 support.zmax = in.at("domain").at("zmax").get<double>();
671 support.rmax = in.at("domain").at("rmax").get<double>();
672 support.parameterisation = in.value("parameterisation", std::string());
673 for (const auto& m : in.at("models")) {
674 Model model;
675 model.thresholdKG = m.at("threshold_kG").get<double>();
676 for (const auto& b : m.at("bands")) {
677 Band band;
678 band.zlo = b.at("zlo").get<double>();
679 band.zhi = b.at("zhi").get<double>();
680 for (const auto& iv : b.at("iv")) {
681 band.iv.push_back({iv.at(0).get<double>(), iv.at(1).get<double>()});
682 }
683 model.bands.push_back(band);
684 }
685 support.models.push_back(model);
686 }
687 } catch (const std::exception& e) {
688 progress(std::string("error: cannot read the support model: ") + e.what());
689 return false;
690 }
691 if (support.models.empty() || support.models.front().bands.empty()) {
692 progress(
693 "error: the support model is empty -- refusing to continue, since an empty model "
694 "would declare the whole geometry field-free");
695 return false;
696 }
697 return true;
698}
699
700// ---------------------------------------------------------------------------
701// the placement table
702// ---------------------------------------------------------------------------
703
704constexpr int kMaxDepth = 14;
705constexpr size_t kMaxRows = 400000;
706constexpr double kSampleDr = 0.05; // cm, radial step of the disproof scan
707constexpr double kSampleArc = 0.5; // cm, azimuthal step of the disproof scan
708constexpr double kSampleDzMax = 2.0; // cm
709constexpr long kMaxSamplesPerRow = 4000000;
710
711struct Row {
712 std::string path, lv, medium, mother, shape;
713 std::string effectiveMother;
714 std::string verdict = "UNCLASSIFIED";
715 int ifield = -1;
716 int copyNo = 0, nDaughters = 0, depth = 0;
717 bool sensitive = false, assembly = false, approximateExtent = false, resolved = true;
718 double zmin = 0., zmax = 0., rmin = 0., rmax = 0.;
719 double separation = -1., penetration = 0.;
720 double maxB = -1., minB = -1.;
721 long nSampled = 0;
722 bool wholeVolumeSampled = false;
723 double wholeMaxB = -1., wholeMinB = -1.;
724 TGeoNode* node = nullptr;
725 TGeoHMatrix matrix;
726};
727
728bool isOutFamily(const std::string& verdict)
729{
730 return verdict == "OUT" || verdict == "OUT_TIGHT" || verdict == "OUT_BOUNDARY";
731}
732
737bool isInFamily(const std::string& verdict)
738{
739 return verdict == "IN" || verdict == "IN_COVERED" || verdict == "UNKNOWN" || verdict == "OUTSIDE_DOMAIN";
740}
741
750bool shapeRadii(TGeoShape* shape, double& rmin, double& rmax)
751{
752 if (auto* pgon = dynamic_cast<TGeoPgon*>(shape)) {
753 rmin = 1e30;
754 rmax = -1e30;
755 for (int i = 0; i < pgon->GetNz(); ++i) {
756 rmin = std::min(rmin, pgon->GetRmin(i));
757 rmax = std::max(rmax, pgon->GetRmax(i));
758 }
759 const double edges = pgon->GetNedges() > 2 ? pgon->GetNedges() : 3;
760 rmax /= std::cos(M_PI / edges);
761 return true;
762 }
763 if (auto* pcon = dynamic_cast<TGeoPcon*>(shape)) {
764 rmin = 1e30;
765 rmax = -1e30;
766 for (int i = 0; i < pcon->GetNz(); ++i) {
767 rmin = std::min(rmin, pcon->GetRmin(i));
768 rmax = std::max(rmax, pcon->GetRmax(i));
769 }
770 return true;
771 }
772 if (auto* cone = dynamic_cast<TGeoCone*>(shape)) {
773 rmin = std::min(cone->GetRmin1(), cone->GetRmin2());
774 rmax = std::max(cone->GetRmax1(), cone->GetRmax2());
775 return true;
776 }
777 if (auto* eltu = dynamic_cast<TGeoEltu*>(shape)) {
778 rmin = 0.; // solid, so it contains the axis
779 rmax = std::max(eltu->GetA(), eltu->GetB());
780 return true;
781 }
782 if (auto* tube = dynamic_cast<TGeoTube*>(shape)) {
783 rmin = tube->GetRmin();
784 rmax = tube->GetRmax();
785 return true;
786 }
787 return false;
788}
789
790bool zPreserving(const TGeoHMatrix& m)
791{
792 const Double_t* r = m.GetRotationMatrix();
793 return std::fabs(std::fabs(r[8]) - 1.) < 1e-9 && std::fabs(r[2]) < 1e-9 && std::fabs(r[5]) < 1e-9 &&
794 std::fabs(r[6]) < 1e-9 && std::fabs(r[7]) < 1e-9;
795}
796
797void extentOf(TGeoNode* node, const TGeoHMatrix& matrix, Row& row)
798{
799 TGeoShape* shape = node->GetVolume()->GetShape();
800 auto* box = dynamic_cast<TGeoBBox*>(shape);
801 if (box == nullptr) {
802 // No bounding box means no analytic extent; claim everything, which classifies
803 // as in-field and never as OUT.
804 row.zmin = row.zmax = 0.;
805 row.rmin = 0.;
806 row.rmax = 1e9;
807 row.approximateExtent = true;
808 return;
809 }
810
811 row.zmin = 1e30;
812 row.zmax = -1e30;
813 double boxRmax = 0.;
814 const double dx = box->GetDX(), dy = box->GetDY(), dz = box->GetDZ();
815 const double* origin = box->GetOrigin();
816 for (int i = 0; i < 8; ++i) {
817 double local[3] = {origin[0] + ((i & 1) ? dx : -dx), origin[1] + ((i & 2) ? dy : -dy),
818 origin[2] + ((i & 4) ? dz : -dz)};
819 double global[3];
820 matrix.LocalToMaster(local, global);
821 row.zmin = std::min(row.zmin, global[2]);
822 row.zmax = std::max(row.zmax, global[2]);
823 boxRmax = std::max(boxRmax, std::hypot(global[0], global[1]));
824 }
825
826 const double* translation = matrix.GetTranslation();
827 const double offAxis = std::hypot(translation[0] + origin[0], translation[1] + origin[1]);
828 double localRmin = 0., localRmax = 0.;
829 if (zPreserving(matrix) && shapeRadii(shape, localRmin, localRmax)) {
830 row.rmin = std::max(0., localRmin - offAxis);
831 row.rmax = localRmax + offAxis;
832 row.approximateExtent = false;
833 } else {
834 row.rmax = boxRmax;
835 row.rmin = (offAxis <= std::hypot(dx, dy)) ? 0. : std::max(0., offAxis - std::hypot(dx, dy));
836 row.approximateExtent = true;
837 }
838}
839
840// ---------------------------------------------------------------------------
841// the doctor
842// ---------------------------------------------------------------------------
843
844struct ContainerProposal {
845 std::string mother, motherPath;
846 double zlo, zhi, rmax;
847 int nDaughters;
848 bool clearedByStrictMargin;
849 bool sensitive;
850};
851
852struct SharedVolume {
853 std::string lv;
854 std::vector<const Row*> out, in;
855};
856
857class Doctor
858{
859 public:
860 Doctor(o2::field::MagneticField* field, const Support& support) : mField(field), mSupport(support)
861 {
862 mThreshold = support.models.front().thresholdKG;
863 }
864
865 void walk(TGeoNode* node) { walk(node, nullptr, TGeoHMatrix(), 0, "", ""); }
866 void classifyAll();
867 void findFindings();
868
869 const std::vector<Row>& rows() const { return mRows; }
870 size_t nPruned() const { return mPruned; }
871 const std::vector<const Row*>& reverseAudit() const { return mReverse; }
872 const std::vector<SharedVolume>& sharedVolumes() const { return mShared; }
873 const std::vector<const Row*>& straddlingMothers() const { return mStraddling; }
874 const std::vector<ContainerProposal>& containers() const { return mContainers; }
875 bool hasSensitive(TGeoVolume* volume);
876
877 private:
878 void walk(TGeoNode* node, TGeoVolume* mother, const TGeoHMatrix& parent, int depth, const std::string& path,
879 const std::string& effectiveMother);
880 void classify(Row& row);
881 void disproofScan(Row& row);
882 void wholeVolumeScan(Row& row);
883 bool ownMaterialAt(const Row& row, const double* global) const;
884
886 const Support& mSupport;
887 double mThreshold;
888 std::vector<Row> mRows;
889 std::map<TGeoVolume*, int> mSensitiveCache;
890 size_t mPruned = 0;
891
892 std::vector<const Row*> mReverse;
893 std::vector<SharedVolume> mShared;
894 std::vector<const Row*> mStraddling;
895 std::vector<ContainerProposal> mContainers;
896};
897
898bool Doctor::hasSensitive(TGeoVolume* volume)
899{
900 auto cached = mSensitiveCache.find(volume);
901 if (cached != mSensitiveCache.end() && cached->second >= 0) {
902 return cached->second == 1;
903 }
904 mSensitiveCache[volume] = 0; // guard against re-entry
905 auto* medium = volume->GetMedium();
906 bool found = medium != nullptr && medium->GetParam(0) != 0.;
907 for (int i = 0; i < volume->GetNdaughters() && !found; ++i) {
908 found = hasSensitive(volume->GetNode(i)->GetVolume());
909 }
910 mSensitiveCache[volume] = found ? 1 : 0;
911 return found;
912}
913
920bool isStructural(TGeoNode* node, TGeoVolume* mother)
921{
922 if (mother == nullptr) {
923 return true;
924 }
925 TGeoVolume* volume = node->GetVolume();
926 if (volume->IsAssembly()) {
927 return true;
928 }
929 auto* mine = volume->GetMedium();
930 auto* theirs = mother->GetMedium();
931 return mine != nullptr && theirs != nullptr && std::strcmp(mine->GetName(), theirs->GetName()) == 0;
932}
933
934void Doctor::walk(TGeoNode* node, TGeoVolume* mother, const TGeoHMatrix& parent, int depth, const std::string& path,
935 const std::string& effectiveMother)
936{
937 if (mRows.size() >= kMaxRows || depth > kMaxDepth) {
938 return;
939 }
940 TGeoHMatrix here = parent;
941 here.Multiply(node->GetMatrix());
942 TGeoVolume* volume = node->GetVolume();
943 const std::string myPath = path + "/" + volume->GetName() + "_" + std::to_string(node->GetNumber());
944
945 Row row;
946 row.path = myPath;
947 row.lv = volume->GetName();
948 row.mother = mother != nullptr ? mother->GetName() : "";
949 row.effectiveMother = effectiveMother;
950 row.shape = volume->GetShape()->ClassName();
951 row.copyNo = node->GetNumber();
952 row.nDaughters = volume->GetNdaughters();
953 row.depth = depth;
954 row.assembly = volume->IsAssembly();
955 auto* medium = volume->GetMedium();
956 row.medium = medium != nullptr ? medium->GetName() : "(none)";
957 row.ifield = medium != nullptr ? (int)medium->GetParam(1) : -1;
958 row.sensitive = medium != nullptr && medium->GetParam(0) != 0.;
959 row.node = node;
960 row.matrix = here;
961 extentOf(node, here, row);
962 mRows.push_back(row);
963
964 if (hasSensitive(volume) && !isStructural(node, mother)) {
965 ++mPruned;
966 return;
967 }
968 for (int i = 0; i < node->GetNdaughters(); ++i) {
969 walk(node->GetDaughter(i), volume, here, depth + 1, myPath, row.assembly ? effectiveMother : myPath);
970 }
971}
972
973// A mother's shape includes the space its daughters occupy, but its MATERIAL is
974// only what they leave over. That distinction is the whole point of these scans:
975// caveRB24's shape reaches the beam axis, and what its daughters leave over is a
976// sliver of cave air at r ~ 3-4 cm, between an oval beam pipe and the circular
977// field cylinder, carrying 13.2 kG.
978bool insideAnyDaughter(TGeoVolume* volume, const double* local)
979{
980 for (int i = 0; i < volume->GetNdaughters(); ++i) {
981 TGeoNode* daughter = volume->GetNode(i);
982 double inDaughter[3];
983 daughter->GetMatrix()->MasterToLocal(local, inDaughter);
984 if (!daughter->GetVolume()->GetShape()->Contains(inDaughter)) {
985 continue;
986 }
987 if (daughter->GetVolume()->IsAssembly()) {
988 if (insideAnyDaughter(daughter->GetVolume(), inDaughter)) {
989 return true;
990 }
991 } else {
992 return true;
993 }
994 }
995 return false;
996}
997
998bool Doctor::ownMaterialAt(const Row& row, const double* global) const
999{
1000 double local[3];
1001 row.matrix.MasterToLocal(global, local);
1002 if (!row.node->GetVolume()->GetShape()->Contains(local)) {
1003 return false;
1004 }
1005 return !insideAnyDaughter(row.node->GetVolume(), local);
1006}
1007
1008double thinnestDaughter(TGeoVolume* volume)
1009{
1010 double thinnest = 1e30;
1011 for (int i = 0; i < volume->GetNdaughters(); ++i) {
1012 auto* box = dynamic_cast<TGeoBBox*>(volume->GetNode(i)->GetVolume()->GetShape());
1013 if (box != nullptr) {
1014 thinnest = std::min(thinnest, 2 * std::min(box->GetDX(), std::min(box->GetDY(), box->GetDZ())));
1015 }
1016 }
1017 return thinnest;
1018}
1019
1023void Doctor::disproofScan(Row& row)
1024{
1025 const double thinnest = thinnestDaughter(row.node->GetVolume());
1026 row.resolved = (row.nDaughters == 0) || (kSampleDr <= std::max(0.1, thinnest));
1027 long budget = kMaxSamplesPerRow;
1028 for (const auto& band : mSupport.models.front().bands) {
1029 const double z0 = std::max(row.zmin, band.zlo);
1030 const double z1 = std::min(row.zmax, band.zhi);
1031 if (z1 < z0) {
1032 continue;
1033 }
1034 for (const auto& iv : band.iv) {
1035 const double r0 = std::max(row.rmin, iv.lo - mSupport.edgeUncertainty);
1036 const double r1 = std::min(row.rmax, iv.hi + mSupport.edgeUncertainty);
1037 if (r1 < r0) {
1038 continue;
1039 }
1040 const double dz = std::min(kSampleDzMax, std::max(0.25, (z1 - z0) / 200.));
1041 for (double z = z0; z <= z1 + 1e-9 && budget > 0; z += dz) {
1042 for (double r = r0; r <= r1 + 1e-9 && budget > 0; r += kSampleDr) {
1043 const int nphi = (r <= 0.) ? 1 : std::max(8, (int)std::ceil(2 * M_PI * r / kSampleArc));
1044 for (int i = 0; i < nphi && budget > 0; ++i) {
1045 const double phi = 2 * M_PI * i / nphi;
1046 double global[3] = {r * std::cos(phi), r * std::sin(phi), z};
1047 --budget;
1048 if (!ownMaterialAt(row, global)) {
1049 continue;
1050 }
1051 ++row.nSampled;
1052 const double b = fieldMag(mField, global[0], global[1], global[2]);
1053 row.maxB = std::max(row.maxB, b);
1054 row.minB = (row.minB < 0.) ? b : std::min(row.minB, b);
1055 }
1056 }
1057 }
1058 }
1059 }
1060 if (budget <= 0) {
1061 row.resolved = false;
1062 }
1063}
1064
1068void Doctor::wholeVolumeScan(Row& row)
1069{
1070 row.wholeVolumeSampled = true;
1071 const double dz = std::max(1.0, (row.zmax - row.zmin) / 300.);
1072 const double dr = std::max(0.5, (row.rmax - row.rmin) / 200.);
1073 long budget = 2000000;
1074 for (double z = row.zmin; z <= row.zmax && budget > 0; z += dz) {
1075 for (double r = row.rmin; r <= row.rmax && budget > 0; r += dr) {
1076 const int nphi = (r <= 0.) ? 1 : std::max(8, (int)std::ceil(2 * M_PI * r / std::max(2.0, dr)));
1077 for (int i = 0; i < nphi && budget > 0; ++i) {
1078 const double phi = 2 * M_PI * i / nphi;
1079 double global[3] = {r * std::cos(phi), r * std::sin(phi), z};
1080 --budget;
1081 if (!ownMaterialAt(row, global)) {
1082 continue;
1083 }
1084 const double b = fieldMag(mField, global[0], global[1], global[2]);
1085 row.wholeMaxB = std::max(row.wholeMaxB, b);
1086 row.wholeMinB = (row.wholeMinB < 0.) ? b : std::min(row.wholeMinB, b);
1087 }
1088 }
1089 }
1090}
1091
1092void Doctor::classify(Row& row)
1093{
1094 if (row.assembly) {
1095 row.verdict = "ASSEMBLY"; // virtual: no material, so nothing to flag
1096 return;
1097 }
1098 if (!mSupport.inDomain(row.zmin, row.zmax, row.rmax)) {
1099 row.verdict = "OUTSIDE_DOMAIN";
1100 return;
1101 }
1102 row.separation = mSupport.separation(0, row.zmin, row.zmax, row.rmin, row.rmax);
1103 if (row.separation >= mSupport.marginStrict) {
1104 row.verdict = "OUT";
1105 return;
1106 }
1107 if (row.separation >= mSupport.marginTight) {
1108 row.verdict = "OUT_TIGHT";
1109 return;
1110 }
1111 row.penetration = mSupport.penetration(0, row.zmin, row.zmax, row.rmin, row.rmax);
1112 if (mSupport.coveredBySupport(0, row.zmin, row.zmax, row.rmin, row.rmax)) {
1113 row.verdict = "IN_COVERED"; // in the field by containment, nothing to disprove
1114 return;
1115 }
1116 disproofScan(row);
1117 if (row.maxB > mThreshold) {
1118 row.verdict = "IN";
1119 } else if (row.separation > 0. || row.penetration <= 2 * mSupport.edgeUncertainty) {
1120 // Touches the support only to within what the model can resolve. Field-freeness
1121 // then rests on an analytic claim about a hard boundary, so it is emitted for a
1122 // human to confirm and never treated as established.
1123 row.verdict = "OUT_BOUNDARY";
1124 } else {
1125 row.verdict = "UNKNOWN";
1126 }
1127}
1128
1129void Doctor::classifyAll()
1130{
1131 size_t done = 0;
1132 for (auto& row : mRows) {
1133 classify(row);
1134 if (++done % 50000 == 0) {
1135 progress(form("classify: %zu / %zu placements", done, mRows.size()));
1136 }
1137 }
1138 for (auto& row : mRows) {
1139 // Only a straddler can be heterogeneous: a placement wholly inside the support
1140 // is uniformly in the field, one that is OUT is uniformly out of it.
1141 if (row.nDaughters > 0 && !row.assembly &&
1142 (row.verdict == "IN" || row.verdict == "UNKNOWN" || row.verdict == "OUT_BOUNDARY")) {
1143 wholeVolumeScan(row);
1144 }
1145 }
1146}
1147
1148void Doctor::findFindings()
1149{
1150 // --- the reverse audit: ifield == 0 media that reach into the field ---------
1151 // An ifield == 0 medium tells the transport engine to move in a straight line.
1152 // Where that is wrong it is a physics bug, so the finding must carry the field
1153 // actually present in the volume's own material, measured. Rows short-circuited
1154 // as IN_COVERED were never sampled, so they are sampled now.
1155 for (auto& row : mRows) {
1156 if (row.assembly || row.ifield != 0 || row.medium == "dummy" || row.medium == "(none)") {
1157 continue;
1158 }
1159 if (isInFamily(row.verdict)) {
1160 if (!row.wholeVolumeSampled) {
1161 wholeVolumeScan(row);
1162 }
1163 mReverse.push_back(&row);
1164 }
1165 }
1166
1167 // --- shared logical volumes placed on both sides (signature C) --------------
1168 std::map<std::string, std::vector<const Row*>> byVolume;
1169 for (const auto& row : mRows) {
1170 if (!row.assembly) {
1171 byVolume[row.lv].push_back(&row);
1172 }
1173 }
1174 for (const auto& entry : byVolume) {
1175 SharedVolume shared;
1176 shared.lv = entry.first;
1177 for (const auto* row : entry.second) {
1178 if (isOutFamily(row->verdict)) {
1179 shared.out.push_back(row);
1180 } else if (isInFamily(row->verdict)) {
1181 shared.in.push_back(row);
1182 }
1183 }
1184 if (!shared.out.empty() && !shared.in.empty()) {
1185 mShared.push_back(shared);
1186 }
1187 }
1188 std::sort(mShared.begin(), mShared.end(),
1189 [](const SharedVolume& a, const SharedVolume& b) { return a.out.size() > b.out.size(); });
1190
1191 // --- mothers whose own material straddles the predicate (signature D) -------
1192 for (const auto& row : mRows) {
1193 if (row.nDaughters > 0 && !row.assembly && row.wholeVolumeSampled && row.wholeMaxB > mThreshold &&
1194 row.wholeMinB <= mThreshold) {
1195 mStraddling.push_back(&row);
1196 }
1197 }
1198
1199 // --- missing containers -----------------------------------------------------
1200 // Where a container belongs is not where the daughters have a spatial gap: a
1201 // beam pipe is a continuous chain of volumes with no gap wider than a flange, so
1202 // gap-based clustering returns one cluster spanning everything and proposes
1203 // nothing. What separates the daughters is the PREDICATE. A maximal run of
1204 // consecutive field-free daughters is exactly a group that cannot be given its
1205 // own medium today, because the only thing enclosing it is the mother's air,
1206 // which is not field-free along its whole length.
1207 for (const auto* mother : mStraddling) {
1208 std::vector<const Row*> kids;
1209 for (const auto& row : mRows) {
1210 if (row.effectiveMother == mother->path && !row.assembly) {
1211 kids.push_back(&row);
1212 }
1213 }
1214 std::sort(kids.begin(), kids.end(), [](const Row* a, const Row* b) { return a->zmin < b->zmin; });
1215
1216 size_t i = 0;
1217 while (i < kids.size()) {
1218 if (!isOutFamily(kids[i]->verdict)) {
1219 ++i;
1220 continue;
1221 }
1222 size_t j = i;
1223 double zlo = kids[i]->zmin, zhi = kids[i]->zmax, rmax = kids[i]->rmax;
1224 while (j + 1 < kids.size() && isOutFamily(kids[j + 1]->verdict)) {
1225 ++j;
1226 zlo = std::min(zlo, kids[j]->zmin);
1227 zhi = std::max(zhi, kids[j]->zmax);
1228 rmax = std::max(rmax, kids[j]->rmax);
1229 }
1230 const int n = (int)(j - i + 1);
1231
1232 // A container must not swallow anything outside its run. Rejecting any run
1233 // with an intruder is too blunt: a 0.05 cm overhang from a neighbouring pipe
1234 // section would kill a 51-daughter proposal. The right answer to a 0.05 cm
1235 // overhang is to move the container's edge, so the edges are clamped past any
1236 // overhanging neighbour and only a residual overlap makes the run unusable.
1237 double clampedLo = zlo, clampedHi = zhi;
1238 std::vector<const Row*> intruders;
1239 for (size_t k = 0; k < kids.size(); ++k) {
1240 if (k >= i && k <= j) {
1241 continue;
1242 }
1243 const Row* other = kids[k];
1244 if (other->zmax <= zlo + 0.01 || other->zmin >= zhi - 0.01 || other->rmin >= rmax - 0.01) {
1245 continue;
1246 }
1247 intruders.push_back(other);
1248 }
1249 for (const Row* other : intruders) {
1250 if (other->zmin <= clampedLo + 1e-9 && other->zmax > clampedLo) {
1251 clampedLo = other->zmax;
1252 }
1253 if (other->zmax >= clampedHi - 1e-9 && other->zmin < clampedHi) {
1254 clampedHi = other->zmin;
1255 }
1256 }
1257 bool residual = false;
1258 for (const Row* other : intruders) {
1259 residual = residual || (other->zmax > clampedLo + 0.01 && other->zmin < clampedHi - 0.01 &&
1260 other->rmin < rmax - 0.01);
1261 }
1262
1263 const double separation =
1264 (clampedHi > clampedLo) ? mSupport.separation(0, clampedLo, clampedHi, 0., rmax) : -1.;
1265 if (n >= 2 && separation >= mSupport.marginTight && !residual) {
1266 ContainerProposal proposal;
1267 proposal.mother = mother->lv;
1268 proposal.motherPath = mother->path;
1269 proposal.zlo = clampedLo;
1270 proposal.zhi = clampedHi;
1271 proposal.rmax = rmax;
1272 proposal.nDaughters = n;
1273 proposal.clearedByStrictMargin = separation >= mSupport.marginStrict;
1274 proposal.sensitive = false;
1275 for (size_t k = i; k <= j; ++k) {
1276 proposal.sensitive = proposal.sensitive || hasSensitive(kids[k]->node->GetVolume());
1277 }
1278 mContainers.push_back(proposal);
1279 }
1280 i = j + 1;
1281 }
1282 }
1283}
1284
1285// ---------------------------------------------------------------------------
1286// outputs
1287// ---------------------------------------------------------------------------
1288
1289void writePlacementCsv(const std::vector<Row>& rows, const std::string& path)
1290{
1291 std::FILE* out = std::fopen(path.c_str(), "w");
1292 if (out == nullptr) {
1293 progress("error: cannot write " + path);
1294 return;
1295 }
1296 std::fprintf(out,
1297 "path,lv,medium,ifield,shape,mother,copy,ndaughters,sensitive,assembly,approx,"
1298 "zmin,zmax,rmin,rmax,verdict,separation_cm,penetration_cm,maxB_kG,nsampled\n");
1299 for (const auto& row : rows) {
1300 std::fprintf(out, "%s,%s,%s,%d,%s,%s,%d,%d,%d,%d,%d,%.3f,%.3f,%.3f,%.3f,%s,%.4f,%.4f,%.6f,%ld\n", row.path.c_str(),
1301 row.lv.c_str(), row.medium.c_str(), row.ifield, row.shape.c_str(), row.mother.c_str(), row.copyNo,
1302 row.nDaughters, (int)row.sensitive, (int)row.assembly, (int)row.approximateExtent, row.zmin, row.zmax,
1303 row.rmin, row.rmax, row.verdict.c_str(), row.separation, row.penetration, row.maxB, row.nSampled);
1304 }
1305 std::fclose(out);
1306}
1307
1308json placementJson(const Row& row)
1309{
1310 return json{{"path", row.path},
1311 {"copy", row.copyNo},
1312 {"z", json::array({row.zmin, row.zmax})},
1313 {"r", json::array({row.rmin, row.rmax})},
1314 {"verdict", row.verdict},
1315 {"separation_cm", row.separation},
1316 {"max_B_kG", std::max(row.maxB, row.wholeMaxB)}};
1317}
1318
1319json proposalsToJson(Doctor& doctor, const Support& support, const std::string& geometryFile,
1320 const std::string& fieldSource)
1321{
1322 const std::time_t now = std::time(nullptr);
1323 char stamp[64];
1324 std::strftime(stamp, sizeof(stamp), "%Y-%m-%dT%H:%M:%S", std::gmtime(&now));
1325
1326 json out;
1327 out["schema"] = "o2-sim-geometry-doctor/proposals/1";
1328 out["generated_utc"] = stamp;
1329 out["geometry"] = geometryFile;
1330 out["field_source"] = fieldSource;
1331 out["threshold_kG"] = support.models.front().thresholdKG;
1332 out["margins_cm"] = {{"strict", support.marginStrict},
1333 {"tight", support.marginTight},
1334 {"edge_uncertainty", support.edgeUncertainty}};
1335 out["proposals"] = json::array();
1336
1337 for (const auto& shared : doctor.sharedVolumes()) {
1338 bool refused = false;
1339 for (const auto* row : shared.out) {
1340 refused = refused || row->sensitive || doctor.hasSensitive(row->node->GetVolume());
1341 }
1342 json entry;
1343 entry["signature"] = "shared-volume";
1344 entry["action"] = "split the logical volume, so that its field-free placements can carry a field-free medium";
1345 entry["logical_volume"] = shared.lv;
1346 entry["n_out"] = shared.out.size();
1347 entry["n_in"] = shared.in.size();
1348 entry["status"] = refused ? "refused by default (sensitive path)" : "proposed";
1349 entry["out_placements"] = json::array();
1350 for (const auto* row : shared.out) {
1351 entry["out_placements"].push_back(placementJson(*row));
1352 }
1353 entry["in_placements"] = json::array();
1354 for (const auto* row : shared.in) {
1355 entry["in_placements"].push_back(placementJson(*row));
1356 }
1357 out["proposals"].push_back(entry);
1358 }
1359
1360 for (const auto* row : doctor.reverseAudit()) {
1361 const double maxB = std::max(row->maxB, row->wholeMaxB);
1362 json entry;
1363 entry["signature"] = "reverse-audit";
1364 entry["action"] = maxB > support.models.front().thresholdKG
1365 ? "the field-free medium assignment is wrong: straight-line transport inside real field"
1366 : "the field-free medium reaches field support but no field was found in its own material, review";
1367 entry["path"] = row->path;
1368 entry["logical_volume"] = row->lv;
1369 entry["medium"] = row->medium;
1370 entry["copy"] = row->copyNo;
1371 entry["z"] = json::array({row->zmin, row->zmax});
1372 entry["r"] = json::array({row->rmin, row->rmax});
1373 entry["min_B_kG"] = row->wholeMinB;
1374 entry["max_B_kG"] = maxB;
1375 entry["verdict"] = row->verdict;
1376 out["proposals"].push_back(entry);
1377 }
1378
1379 for (const auto& container : doctor.containers()) {
1380 json entry;
1381 entry["signature"] = "missing-container";
1382 entry["action"] = "insert a container with a field-free medium and re-parent the cluster into it";
1383 entry["mother"] = container.mother;
1384 entry["mother_path"] = container.motherPath;
1385 entry["z"] = json::array({container.zlo, container.zhi});
1386 entry["rmax"] = container.rmax;
1387 entry["n_daughters"] = container.nDaughters;
1388 entry["status"] = container.sensitive ? "refused by default (sensitive path)" : "proposed";
1389 entry["clearance"] = container.clearedByStrictMargin ? "strict margin" : "tight margin";
1390 out["proposals"].push_back(entry);
1391 }
1392
1393 for (const auto* row : doctor.straddlingMothers()) {
1394 json entry;
1395 entry["signature"] = "heterogeneous-mother";
1396 entry["action"] =
1397 "the mother's own material spans both sides of the predicate, so no per-medium flag can "
1398 "express it; it needs a container";
1399 entry["path"] = row->path;
1400 entry["logical_volume"] = row->lv;
1401 entry["medium"] = row->medium;
1402 entry["min_B_kG"] = row->wholeMinB;
1403 entry["max_B_kG"] = row->wholeMaxB;
1404 entry["n_daughters"] = row->nDaughters;
1405 out["proposals"].push_back(entry);
1406 }
1407 return out;
1408}
1409
1410// ---------------------------------------------------------------------------
1411// the anchor self-check
1412// ---------------------------------------------------------------------------
1413
1414// A unit test for this tool would need a placed ALICE geometry and a field map,
1415// neither of which belongs in the repository, so the regression gate is instead a
1416// file of expectations that any ALICE Run 3 geometry must satisfy, checked against
1417// a real run with --verify-anchors. See run/geometry-doctor-anchors.json.
1418//
1419// Supported expectations: OUT (every placement of the volume is in the OUT
1420// family), IN, NOT_OUT, ASSEMBLY, and REVERSE_AUDIT_FLAGGED. An anchor may also
1421// require a placement count and a lower bound on the field found in the volume's
1422// own material.
1423
1424bool verifyAnchors(const std::string& path, Doctor& doctor, Report& report)
1425{
1426 std::ifstream in(path);
1427 if (!in) {
1428 report(" cannot open the anchor file " + path);
1429 return false;
1430 }
1431 json anchors;
1432 try {
1433 in >> anchors;
1434 } catch (const std::exception& e) {
1435 report(std::string(" cannot parse the anchor file: ") + e.what());
1436 return false;
1437 }
1438
1439 std::set<std::string> flaggedByReverseAudit;
1440 for (const auto* row : doctor.reverseAudit()) {
1441 flaggedByReverseAudit.insert(row->lv);
1442 }
1443
1444 bool allPassed = true;
1445 report(form(" %-22s %-22s %-10s %s", "volume", "expected", "verdict", "evidence"));
1446 for (const auto& anchor : anchors.at("anchors")) {
1447 const auto volume = anchor.at("volume").get<std::string>();
1448 const auto expected = anchor.at("expect").get<std::string>();
1449
1450 int placements = 0;
1451 int failures = 0;
1452 double worstSeparation = 1e30;
1453 double bestField = -1.;
1454 std::string reported;
1455 for (const auto& row : doctor.rows()) {
1456 if (row.lv != volume) {
1457 continue;
1458 }
1459 ++placements;
1460 bool ok = true;
1461 if (expected == "OUT") {
1462 ok = isOutFamily(row.verdict) || row.verdict == "ASSEMBLY";
1463 } else if (expected == "IN") {
1464 ok = row.verdict == "IN";
1465 } else if (expected == "NOT_OUT") {
1466 ok = !isOutFamily(row.verdict);
1467 } else if (expected == "ASSEMBLY") {
1468 ok = row.verdict == "ASSEMBLY";
1469 } else if (expected == "REVERSE_AUDIT_FLAGGED") {
1470 ok = true; // decided below, on the volume rather than the placement
1471 } else {
1472 report(" unknown expectation '" + expected + "' for " + volume);
1473 return false;
1474 }
1475 failures += ok ? 0 : 1;
1476 bestField = std::max(bestField, std::max(row.maxB, row.wholeMaxB));
1477 if (row.separation < worstSeparation) {
1478 worstSeparation = row.separation;
1479 reported = row.verdict;
1480 }
1481 }
1482
1483 if (expected == "REVERSE_AUDIT_FLAGGED") {
1484 failures = flaggedByReverseAudit.count(volume) > 0 ? 0 : 1;
1485 reported = failures == 0 ? "flagged" : "not flagged";
1486 }
1487 if (placements == 0) {
1488 failures = 1;
1489 reported = "not placed";
1490 }
1491 if (anchor.contains("placements") && placements != anchor.at("placements").get<int>()) {
1492 failures += 1;
1493 }
1494 if (anchor.contains("min_max_B_kG") && bestField < anchor.at("min_max_B_kG").get<double>()) {
1495 failures += 1;
1496 }
1497
1498 allPassed = allPassed && failures == 0;
1499 report(form(" %-22s %-22s %-10s %d placement(s), separation %.3f cm, max|B| %.4f kG %s", volume.c_str(),
1500 expected.c_str(), reported.c_str(), placements, worstSeparation > 1e29 ? -1. : worstSeparation,
1501 bestField, failures == 0 ? "PASS" : "FAIL"));
1502 }
1503 return allPassed;
1504}
1505
1506// ---------------------------------------------------------------------------
1507
1508struct Options {
1509 std::string geometryFile;
1510 std::string fieldFile;
1511 int fieldCurrent = 0;
1512 std::string supportFile;
1513 std::string anchorFile;
1514 std::vector<double> thresholdsGauss;
1515 double margin = 5.0;
1516 std::string outputPrefix = "geometry-doctor";
1517};
1518
1519} // namespace
1520
1521int main(int argc, char** argv)
1522{
1523 Options options;
1524 bpo::options_description description(
1525 "Audits a placed geometry against the magnetic field it will be transported "
1526 "in, and reports where the two do not fit together.\n\nOptions");
1527 description.add_options() //
1528 ("help,h", "print this help message") //
1529 ("geometry-file", bpo::value<std::string>(&options.geometryFile)->required(), //
1530 "the geometry to audit, e.g. o2sim_geometry.root") //
1531 ("field-file", bpo::value<std::string>(&options.fieldFile), //
1532 "a serialized MagneticField carrying reference probe vectors") //
1533 ("field-current", bpo::value<int>(&options.fieldCurrent), //
1534 "build the nominal field for this L3 current instead, e.g. -5") //
1535 ("support-file", bpo::value<std::string>(&options.supportFile), //
1536 "field-support model cache: read it if it exists, otherwise write it") //
1537 ("threshold", bpo::value<std::vector<double>>(&options.thresholdsGauss)->composing(), //
1538 "field threshold in Gauss, repeatable; the lowest one decides the verdicts (default 1 and 10)") //
1539 ("margin", bpo::value<double>(&options.margin)->default_value(5.0), //
1540 "clearance in cm a placement must keep from the field support to be called field-free") //
1541 ("output-prefix", bpo::value<std::string>(&options.outputPrefix)->default_value("geometry-doctor"), //
1542 "prefix for the report, the proposals and the placement table") //
1543 ("verify-anchors", bpo::value<std::string>(&options.anchorFile), //
1544 "check the classification against known-good volumes listed in this JSON file");
1545
1546 bpo::variables_map arguments;
1547 try {
1548 bpo::store(bpo::parse_command_line(argc, argv, description), arguments);
1549 if (arguments.count("help") != 0u) {
1550 std::cout << description << '\n';
1551 return 0;
1552 }
1553 bpo::notify(arguments);
1554 } catch (const bpo::error& e) {
1555 std::cerr << "error: " << e.what() << "\n\n"
1556 << description << '\n';
1557 return 1;
1558 }
1559
1560 const bool haveFieldFile = arguments.count("field-file") != 0u;
1561 const bool haveFieldCurrent = arguments.count("field-current") != 0u;
1562 if (haveFieldFile == haveFieldCurrent) {
1563 std::cerr << "error: give exactly one of --field-file and --field-current\n";
1564 return 1;
1565 }
1566 if (options.thresholdsGauss.empty()) {
1567 options.thresholdsGauss = {1., 10.};
1568 }
1569 std::sort(options.thresholdsGauss.begin(), options.thresholdsGauss.end());
1570 std::vector<double> thresholds; // kGauss, as the field itself reports
1571 for (double gauss : options.thresholdsGauss) {
1572 thresholds.push_back(gauss * 1e-3);
1573 }
1574
1575 const std::string fieldSource =
1576 haveFieldFile ? options.fieldFile : form("createNominalField(%d)", options.fieldCurrent);
1578 haveFieldFile ? loadFieldFromFile(options.fieldFile) : o2::field::MagneticField::createNominalField(options.fieldCurrent);
1579 if (field == nullptr) {
1580 std::cerr << "error: no usable magnetic field\n";
1581 return 1;
1582 }
1583
1584 Report report;
1585 report("ALICE simulation geometry doctor");
1586 report("");
1587 report(" geometry : " + options.geometryFile);
1588 report(" field : " + fieldSource + ", parameterisation " + field->getParameterName());
1589
1590 // --- the field-support model ------------------------------------------------
1591 Support support;
1592 bool supportFromCache = false;
1593 if (!options.supportFile.empty()) {
1594 std::ifstream cache(options.supportFile);
1595 if (cache) {
1596 json cached;
1597 try {
1598 cache >> cached;
1599 } catch (const std::exception& e) {
1600 std::cerr << "error: cannot parse " << options.supportFile << ": " << e.what() << '\n';
1601 return 1;
1602 }
1603 if (!supportFromJson(cached, support)) {
1604 return 1;
1605 }
1606 supportFromCache = true;
1607 }
1608 }
1609
1610 if (supportFromCache) {
1611 if (support.models.size() != thresholds.size()) {
1612 std::cerr << "error: " << options.supportFile << " carries " << support.models.size()
1613 << " thresholds but " << thresholds.size() << " were requested\n";
1614 return 1;
1615 }
1616 for (size_t t = 0; t < thresholds.size(); ++t) {
1617 if (std::fabs(support.models[t].thresholdKG - thresholds[t]) > 1e-9) {
1618 std::cerr << "error: " << options.supportFile << " was built for a different threshold ("
1619 << support.models[t].thresholdKG * 1000. << " G against " << thresholds[t] * 1000. << " G)\n";
1620 return 1;
1621 }
1622 }
1623 if (!support.parameterisation.empty() && support.parameterisation != field->getParameterName()) {
1624 std::cerr << "error: " << options.supportFile << " was built for parameterisation "
1625 << support.parameterisation << ", not " << field->getParameterName() << '\n';
1626 return 1;
1627 }
1628 report(" support model : " + options.supportFile + " (cached)");
1629 } else {
1630 support = buildSupport(field, thresholds, -3000., 3000.);
1631 if (!options.supportFile.empty()) {
1632 std::ofstream out(options.supportFile);
1633 out << supportToJson(support, fieldSource).dump(1, '\t') << '\n';
1634 report(" support model : built and written to " + options.supportFile);
1635 } else {
1636 report(" support model : built for this run");
1637 }
1638 }
1639 support.marginStrict = options.margin;
1640
1641 std::string bandCounts;
1642 for (const auto& model : support.models) {
1643 bandCounts += form("%s%.1f G -> %zu bands", bandCounts.empty() ? "" : ", ", model.thresholdKG * 1000.,
1644 model.bands.size());
1645 }
1646 report(" " + bandCounts);
1647 report(form(" margins: strict %.2f cm, tight %.2f cm, edge uncertainty %.2f cm",
1648 support.marginStrict, support.marginTight, support.edgeUncertainty));
1649 report("");
1650
1651 // The model is only worth anything if it really is an outer bound, and only the
1652 // field itself can say so.
1653 report("outer-bound check");
1654 if (!violationScan(field, support, report)) {
1655 report("");
1656 report("The support model is not an outer bound on this field, so no placement can be called");
1657 report("field-free from it. Refusing to classify.");
1658 report.write(options.outputPrefix + "-report.txt");
1659 return 1;
1660 }
1661 report("");
1662
1663 // --- the geometry -----------------------------------------------------------
1664 TGeoManager::Import(options.geometryFile.c_str());
1665 if (gGeoManager == nullptr) {
1666 std::cerr << "error: no TGeoManager in " << options.geometryFile << '\n';
1667 return 1;
1668 }
1669 report(form(" volumes : %d, media %d", gGeoManager->GetListOfVolumes()->GetEntries(),
1670 gGeoManager->GetListOfMedia()->GetEntries()));
1671
1672 Doctor doctor(field, support);
1673 doctor.walk(gGeoManager->GetTopNode());
1674 report(form(" placements : %zu classified, %zu detector subtrees pruned", doctor.rows().size(),
1675 doctor.nPruned()));
1676 report("");
1677
1678 progress("classify: sampling the field inside every placement that reaches the support");
1679 doctor.classifyAll();
1680 doctor.findFindings();
1681
1682 std::map<std::string, int> verdicts;
1683 for (const auto& row : doctor.rows()) {
1684 ++verdicts[row.verdict];
1685 }
1686 report("verdicts");
1687 for (const auto& verdict : verdicts) {
1688 report(form(" %-16s %7d", verdict.first.c_str(), verdict.second));
1689 }
1690 report("");
1691
1692 // --- the reverse audit ------------------------------------------------------
1693 const double threshold = support.models.front().thresholdKG;
1694 std::map<std::string, std::pair<int, double>> reverseByVolume;
1695 for (const auto* row : doctor.reverseAudit()) {
1696 auto& entry = reverseByVolume[row->lv + " [" + row->medium + "]"];
1697 ++entry.first;
1698 entry.second = std::max(entry.second, std::max(row->maxB, row->wholeMaxB));
1699 }
1700 int inRealField = 0;
1701 for (const auto& entry : reverseByVolume) {
1702 inRealField += entry.second.second > threshold ? 1 : 0;
1703 }
1704 report(form("reverse audit: %zu placements carry a field-free medium yet reach into the field support",
1705 doctor.reverseAudit().size()));
1706 report(form(" %zu logical volumes, %d of them with real field in their own material",
1707 reverseByVolume.size(), inRealField));
1708 report(form(" %-46s %11s %16s", "volume [medium]", "placements", "max |B| [kG]"));
1709 for (const auto& entry : reverseByVolume) {
1710 report(form(" %-46s %11d %16.4f%s", entry.first.c_str(), entry.second.first, entry.second.second,
1711 entry.second.second > threshold ? " <-- straight-line transport in real field" : ""));
1712 }
1713 report("");
1714
1715 // --- the forward findings ---------------------------------------------------
1716 report(form("shared volumes: %zu logical volumes are placed both out of and into the field",
1717 doctor.sharedVolumes().size()));
1718 report(form(" %-28s %8s %8s %s", "logical volume", "out", "in", "status"));
1719 for (size_t i = 0; i < doctor.sharedVolumes().size() && i < 20; ++i) {
1720 const auto& shared = doctor.sharedVolumes()[i];
1721 bool refused = false;
1722 bool approximate = false;
1723 for (const auto* row : shared.out) {
1724 refused = refused || row->sensitive || doctor.hasSensitive(row->node->GetVolume());
1725 approximate = approximate || row->approximateExtent;
1726 }
1727 report(form(" %-28s %8zu %8zu %s%s", shared.lv.c_str(), shared.out.size(), shared.in.size(),
1728 refused ? "refused by default (sensitive)" : "proposed",
1729 approximate ? " [extent approximate]" : ""));
1730 }
1731 if (doctor.sharedVolumes().size() > 20) {
1732 report(form(" ... and %zu more, all of them in the proposals file", doctor.sharedVolumes().size() - 20));
1733 }
1734 report("");
1735
1736 report(form("heterogeneous mothers: %zu whose own material straddles the predicate",
1737 doctor.straddlingMothers().size()));
1738 for (size_t i = 0; i < doctor.straddlingMothers().size() && i < 10; ++i) {
1739 const auto* row = doctor.straddlingMothers()[i];
1740 report(form(" %-40s |B| in own material %.4g .. %.4f kG, %d daughters", row->lv.c_str(), row->wholeMinB,
1741 row->wholeMaxB, row->nDaughters));
1742 }
1743 report("");
1744
1745 report(form("missing containers: %zu daughter clusters lie wholly on the field-free side",
1746 doctor.containers().size()));
1747 for (const auto& container : doctor.containers()) {
1748 report(form(" in %-14s z %9.2f .. %9.2f rmax %7.2f %3d daughters %s%s", container.mother.c_str(),
1749 container.zlo, container.zhi, container.rmax, container.nDaughters,
1750 container.clearedByStrictMargin ? "clear by the strict margin" : "clear by the tight margin",
1751 container.sensitive ? " [refused: sensitive]" : ""));
1752 }
1753 report("");
1754
1755 // --- the anchor self-check --------------------------------------------------
1756 bool anchorsPassed = true;
1757 if (!options.anchorFile.empty()) {
1758 report("anchors");
1759 anchorsPassed = verifyAnchors(options.anchorFile, doctor, report);
1760 report(anchorsPassed ? " all anchors reproduced" : " ANCHORS FAILED");
1761 report("");
1762 }
1763
1764 // --- outputs ----------------------------------------------------------------
1765 const std::string proposalsPath = options.outputPrefix + "-proposals.json";
1766 const std::string tablePath = options.outputPrefix + "-placements.csv";
1767 const std::string reportPath = options.outputPrefix + "-report.txt";
1768 std::ofstream proposals(proposalsPath);
1769 proposals << proposalsToJson(doctor, support, options.geometryFile, fieldSource).dump(1, '\t') << '\n';
1770 writePlacementCsv(doctor.rows(), tablePath);
1771 report("wrote " + proposalsPath + ", " + tablePath + " and " + reportPath);
1772 report.write(reportPath);
1773
1774 return anchorsPassed ? 0 : 2;
1775}
header::DataOrigin origin
header::DataDescription description
std::unique_ptr< expressions::Node > node
int32_t i
bool done
Definition of the MagF class.
uint32_t j
Definition RawData.h:0
nlohmann::json json
Double_t getFactorDipole() const
Return the sign*scale of the current in the Dipole according to sPolarityConventionthe.
void setDataFileName(const Char_t *nm)
void setFactorDipole(float fc=1.)
Sets the sign*scale of the current in the Dipole according to sPolarityConvention.
Double_t getFactorSolenoid() const
Returns the sign*scale of the current in the Dipole according to sPolarityConventionthe.
void Field(const Double_t *__restrict__ point, Double_t *__restrict__ bField) override
Char_t * getParameterName() const
void setFactorSolenoid(float fc=1.)
Sets the sign/scale of the current in the L3 according to sPolarityConvention.
void CreateField()
real field creation is here
static MagneticField * createNominalField(int fld, bool uniform=false)
create field from rounded value, i.e. +-5 or +-2 kGauss
GLdouble n
Definition glcorearb.h:1982
GLint GLenum GLint x
Definition glcorearb.h:403
const GLfloat * m
Definition glcorearb.h:4066
GLuint segment
Definition glcorearb.h:4945
GLuint buffer
Definition glcorearb.h:655
GLuint entry
Definition glcorearb.h:5735
GLsizeiptr size
Definition glcorearb.h:659
GLboolean GLboolean GLboolean b
Definition glcorearb.h:1233
GLint y
Definition glcorearb.h:270
GLint reference
Definition glcorearb.h:5487
GLint GLint GLsizei GLsizei GLsizei depth
Definition glcorearb.h:470
GLsizei const GLchar *const * path
Definition glcorearb.h:3591
GLboolean r
Definition glcorearb.h:1233
GLboolean GLboolean GLboolean GLboolean a
Definition glcorearb.h:1233
GLsizei const GLint * box
Definition glcorearb.h:4697
GLdouble GLdouble GLdouble z
Definition glcorearb.h:843
void report(gsl::span< o2::InteractionTimeRecord > irs, int threshold, bool verbose)
bpo::variables_map arguments
std::string to_string(gsl::span< T, Size > span)
Definition common.h:52
std::map< std::string, ID > expected
VectorOfTObjectPtrs other
#define main
std::vector< int > row
std::vector< ReadoutWindowData > rows