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
43
45#include "Field/MagneticField.h"
46
47#include <TFile.h>
48#include <TGeoBBox.h>
49#include <TGeoCone.h>
50#include <TGeoEltu.h>
51#include <TGeoManager.h>
52#include <TGeoMatrix.h>
53#include <TGeoMedium.h>
54#include <TGeoNavigator.h>
55#include <TGeoNode.h>
56#include <TGeoPcon.h>
57#include <TGeoPgon.h>
58#include <TGeoShape.h>
59#include <TGeoTube.h>
60#include <TGeoVolume.h>
61#include <TRandom3.h>
62#include <TVectorD.h>
63
64#include <boost/program_options.hpp>
65#include <nlohmann/json.hpp>
66
67#include <algorithm>
68#include <atomic>
69#include <cmath>
70#include <cstdio>
71#include <cstring>
72#include <ctime>
73#include <fstream>
74#include <iostream>
75#include <map>
76#include <set>
77#include <string>
78#include <thread>
79#include <utility>
80#include <vector>
81
82namespace bpo = boost::program_options;
83using json = nlohmann::json;
84
85namespace
86{
87
88// ---------------------------------------------------------------------------
89// small helpers
90// ---------------------------------------------------------------------------
91
93template <typename... Args>
94std::string form(const char* fmt, Args... args)
95{
96 char buffer[4096];
97 std::snprintf(buffer, sizeof(buffer), fmt, args...);
98 return std::string(buffer);
99}
100
102class Report
103{
104 public:
105 void operator()(const std::string& line)
106 {
107 std::cout << line << '\n';
108 mLines.push_back(line);
109 }
110 void write(const std::string& path) const
111 {
112 std::ofstream out(path);
113 for (const auto& line : mLines) {
114 out << line << '\n';
115 }
116 }
117
118 private:
119 std::vector<std::string> mLines;
120};
121
123void progress(const std::string& line) { std::cerr << line << std::endl; }
124
125// ---------------------------------------------------------------------------
126// the field
127// ---------------------------------------------------------------------------
128
129// A serialized MagneticField keeps its measured map in a transient member, so a
130// reader has to call CreateField() again to get a usable field back. That call
131// carries a trap: it feeds mMultipicativeFactorSolenoid/Dipole back through
132// setters which negate under the LHC polarity convention, so CreateField() is not
133// idempotent and the second call inverts the polarity of both the measured map
134// and the machine compensators. |B| is untouched, which is exactly why the flip
135// survives any magnitude-based check -- a 200k-point round trip on |B| passes
136// while every field vector points the wrong way.
137//
138// A field file therefore has to carry reference field VECTORS taken from the live
139// object at write time. The loader re-evaluates them, repairs a pure global flip,
140// and refuses the file if what it gets back is anything other than what was
141// written. A file without those probes cannot be verified and is refused too;
142// --field-current builds the field from scratch instead.
143
144constexpr const char* kFieldObjectKey = "MagneticField";
145constexpr const char* kFieldProbeKey = "ReferenceProbes";
146
148int compareToProbes(o2::field::MagneticField* field, const TVectorD& probes)
149{
150 const int n = probes.GetNrows() / 6;
151 bool same = true;
152 bool flipped = true;
153 for (int i = 0; i < n; ++i) {
154 double x[3] = {probes[6 * i], probes[6 * i + 1], probes[6 * i + 2]};
155 double b[3] = {0., 0., 0.};
156 field->Field(x, b);
157 for (int k = 0; k < 3; ++k) {
158 const double want = probes[6 * i + 3 + k];
159 same = same && (b[k] == want);
160 flipped = flipped && (b[k] == -want);
161 }
162 }
163 return same ? 0 : (flipped ? 1 : -1);
164}
165
166o2::field::MagneticField* loadFieldFromFile(const std::string& path)
167{
168 TFile* file = TFile::Open(path.c_str());
169 if (file == nullptr || file->IsZombie()) {
170 progress("error: cannot open field file " + path);
171 return nullptr;
172 }
173 auto* field = dynamic_cast<o2::field::MagneticField*>(file->Get(kFieldObjectKey));
174 auto* probes = dynamic_cast<TVectorD*>(file->Get(kFieldProbeKey));
175 if (field == nullptr) {
176 progress(form("error: no '%s' object in %s", kFieldObjectKey, path.c_str()));
177 return nullptr;
178 }
179 if (probes == nullptr) {
180 progress(form(
181 "error: no '%s' in %s -- the field cannot be verified against what was written, "
182 "and a silently inverted field is exactly what this check exists to catch. "
183 "Use --field-current instead.",
184 kFieldProbeKey, path.c_str()));
185 return nullptr;
186 }
187 const TVectorD reference(*probes);
188 file->Close();
189 delete file;
190
191 // Reload the parameterisation from this file, whatever path was stored at write
192 // time, so that the file is self-contained and relocatable.
193 field->setDataFileName(path.c_str());
194 field->CreateField();
195
196 int comparison = compareToProbes(field, reference);
197 if (comparison == 1) {
198 field->setFactorSolenoid(-field->getFactorSolenoid());
199 field->setFactorDipole(-field->getFactorDipole());
200 comparison = compareToProbes(field, reference);
201 if (comparison == 0) {
202 progress("field: polarity flip from the non-idempotent CreateField() detected and repaired");
203 }
204 }
205 if (comparison != 0) {
206 progress(form(
207 "error: the field reloaded from %s does not reproduce its own reference probes; "
208 "refusing to hand back a field that is not the one written",
209 path.c_str()));
210 return nullptr;
211 }
212 progress(form("field: %s verified against %d reference probe vectors", path.c_str(), reference.GetNrows() / 6));
213 return field;
214}
215
216double fieldMag(o2::field::MagneticField* field, double x, double y, double z)
217{
218 double point[3] = {x, y, z};
219 double b[3] = {0., 0., 0.};
220 field->Field(point, b);
221 return std::sqrt(b[0] * b[0] + b[1] * b[1] + b[2] * b[2]);
222}
223
224double fieldMagCyl(o2::field::MagneticField* field, double r, double phi, double z)
225{
226 return fieldMag(field, r * std::cos(phi), r * std::sin(phi), z);
227}
228
229// ---------------------------------------------------------------------------
230// the field-support model
231// ---------------------------------------------------------------------------
232
233// The model states, per threshold, a list of z-bands each carrying the radial
234// intervals in which |B| exceeds that threshold, maximised over phi. It is an
235// OUTER bound: a point outside every band has |B| <= threshold. That direction is
236// what lets a sampled quantity support a geometric argument about a volume.
237//
238// Two features of the real field dictate the sampling, and both were found by
239// this model getting them wrong first:
240//
241// * the LHC machine elements are hard cylinders with a discontinuous edge (the
242// A-side compensator aperture is exactly r < 4.0 cm), so every threshold
243// crossing is bisected rather than left on the grid;
244//
245// * the measured map's coverage is a BOX in (x, y), not a cylinder. Between the
246// box's inscribed and corner radius the field survives only in ~2 degree wedges
247// at the four corners -- at r = 194.5, z = -797.9 it is 8.2 kG at phi = 132.8
248// degrees and exactly zero at 127.5 and 135. A model sampling 16 phi values
249// steps straight over those wedges and declares 8 kG of dipole field
250// unsupported. phi sampling is therefore bounded by ARC LENGTH, so the angular
251// resolution follows the feature size at every radius.
252//
253// Interval edges are stored as best estimates with a separate edge uncertainty
254// rather than pre-inflated, because the study this tool comes from turns on a
255// volume whose inner radius is exactly the field boundary: it is separated from
256// the field by exactly zero, and no amount of inflation may promote that to a
257// margin.
258
259constexpr int kMinPhiSamples = 24;
260constexpr double kPhiArcStep = 3.0; // cm, the azimuthal sampling bound
261constexpr double kBisectionTol = 0.01; // cm, also the model's edge uncertainty
262constexpr double kZStepCoarse = 1.0; // cm
263constexpr double kZStepRefine = 0.1; // cm, used wherever the radial structure changes
264constexpr double kScanRMax = 2100.; // cm, outer reach of the scan
265constexpr double kScanRMaxFine = 900.; // cm, beyond this the radial grid is coarse
266constexpr double kTightMargin = 0.05; // cm, for boundaries that are analytically hard
267constexpr long kViolationScanPoints = 400000;
268
269struct Interval {
270 double lo, hi;
271};
272
273struct Band {
274 double zlo, zhi;
275 std::vector<Interval> iv;
276};
277
278struct Model {
279 double thresholdKG = 0.;
280 std::vector<Band> bands;
281};
282
283struct Support {
284 std::vector<Model> models;
285 double edgeUncertainty = kBisectionTol;
286 double marginStrict = 5.0;
287 double marginTight = kTightMargin;
288 double zmin = 0., zmax = 0., rmax = 0.;
289 std::string parameterisation;
290
295 double separation(int t, double vzmin, double vzmax, double vrmin, double vrmax) const
296 {
297 double best = 1e30;
298 for (const auto& band : models[t].bands) {
299 const double dz = std::max(0., std::max(band.zlo - vzmax, vzmin - band.zhi));
300 for (const auto& iv : band.iv) {
301 const double lo = iv.lo - edgeUncertainty;
302 const double hi = iv.hi + edgeUncertainty;
303 const double dr = std::max(0., std::max(lo - vrmax, vrmin - hi));
304 best = std::min(best, std::sqrt(dz * dz + dr * dr));
305 if (best <= 0.) {
306 return 0.;
307 }
308 }
309 }
310 return (best > 1e29) ? 1e30 : best;
311 }
312
313 bool supportAt(int t, double z, double r) const { return separation(t, z, z, r, r) <= 0.; }
314
319 double penetration(int t, double vzmin, double vzmax, double vrmin, double vrmax) const
320 {
321 double worst = 0.;
322 for (const auto& band : models[t].bands) {
323 if (band.zlo > vzmax || band.zhi < vzmin) {
324 continue;
325 }
326 for (const auto& iv : band.iv) {
327 const double lo = iv.lo - edgeUncertainty;
328 const double hi = iv.hi + edgeUncertainty;
329 if (lo > vrmax || hi < vrmin) {
330 continue;
331 }
332 worst = std::max(worst, std::min(hi - vrmin, vrmax - lo));
333 }
334 }
335 return worst;
336 }
337
345 bool coveredBySupport(int t, double vzmin, double vzmax, double vrmin, double vrmax) const
346 {
347 std::vector<std::pair<double, double>> covering;
348 for (const auto& band : models[t].bands) {
349 if (band.zhi < vzmin || band.zlo > vzmax) {
350 continue;
351 }
352 for (const auto& iv : band.iv) {
353 if (iv.lo - edgeUncertainty <= vrmin && iv.hi + edgeUncertainty >= vrmax) {
354 covering.push_back({band.zlo, band.zhi});
355 break;
356 }
357 }
358 }
359 std::sort(covering.begin(), covering.end());
360 double frontier = vzmin;
361 for (const auto& segment : covering) {
362 if (segment.first > frontier + 1e-9) {
363 return false;
364 }
365 frontier = std::max(frontier, segment.second);
366 if (frontier >= vzmax) {
367 return true;
368 }
369 }
370 return frontier >= vzmax;
371 }
372
374 bool inDomain(double vzmin, double vzmax, double vrmax) const
375 {
376 return vzmin >= zmin && vzmax <= zmax && vrmax <= rmax;
377 }
378};
379
380int phiSamplesAt(double r)
381{
382 if (r <= 0.) {
383 return 1;
384 }
385 return std::max(kMinPhiSamples, (int)std::ceil(2 * M_PI * r / kPhiArcStep));
386}
387
388double maxFieldOverPhi(o2::field::MagneticField* field, double r, double z)
389{
390 if (r == 0.) {
391 return fieldMag(field, 0., 0., z);
392 }
393 const int n = phiSamplesAt(r);
394 double worst = 0.;
395 for (int i = 0; i < n; ++i) {
396 worst = std::max(worst, fieldMagCyl(field, r, 2 * M_PI * i / n, z));
397 }
398 return worst;
399}
400
401std::vector<double> radialGrid()
402{
403 std::vector<double> grid;
404 for (double r = 0.; r < 20.; r += 0.1) {
405 grid.push_back(r);
406 }
407 for (double r = 20.; r < 100.; r += 1.0) {
408 grid.push_back(r);
409 }
410 for (double r = 100.; r < 800.; r += 2.0) {
411 grid.push_back(r);
412 }
413 for (double r = 800.; r <= kScanRMaxFine; r += 10.0) {
414 grid.push_back(r);
415 }
416 // A coarse extension so that an unexpected far feature is not invisible by construction.
417 for (double r = kScanRMaxFine + 25.; r <= kScanRMax; r += 25.0) {
418 grid.push_back(r);
419 }
420 return grid;
421}
422
427double bisectCrossing(o2::field::MagneticField* field, double z, double rOut, double rIn, double threshold)
428{
429 for (int i = 0; i < 60 && std::fabs(rOut - rIn) > kBisectionTol; ++i) {
430 const double middle = 0.5 * (rOut + rIn);
431 if (maxFieldOverPhi(field, middle, z) > threshold) {
432 rIn = middle;
433 } else {
434 rOut = middle;
435 }
436 }
437 return 0.5 * (rOut + rIn);
438}
439
440struct Slice {
441 double z = 0., zlo = 0., zhi = 0.;
442 std::vector<std::vector<Interval>> iv; // one per threshold
443};
444
445Slice sliceAt(o2::field::MagneticField* field, double z, const std::vector<double>& thresholds)
446{
447 Slice slice;
448 slice.z = z;
449 slice.iv.resize(thresholds.size());
450 const std::vector<double> grid = radialGrid();
451 std::vector<double> b(grid.size());
452 for (size_t i = 0; i < grid.size(); ++i) {
453 b[i] = maxFieldOverPhi(field, grid[i], z);
454 }
455 for (size_t t = 0; t < thresholds.size(); ++t) {
456 bool open = false;
457 Interval current{0., 0.};
458 for (size_t i = 0; i < grid.size(); ++i) {
459 const bool above = b[i] > thresholds[t];
460 if (above && !open) {
461 current.lo = (i > 0) ? std::max(0., bisectCrossing(field, z, grid[i - 1], grid[i], thresholds[t])) : grid[i];
462 open = true;
463 } else if (!above && open) {
464 current.hi = bisectCrossing(field, z, grid[i], grid[i - 1], thresholds[t]);
465 slice.iv[t].push_back(current);
466 open = false;
467 }
468 }
469 if (open) {
470 current.hi = grid.back();
471 slice.iv[t].push_back(current);
472 }
473 }
474 return slice;
475}
476
477void mergeIntervals(std::vector<Interval>& into, const std::vector<Interval>& from)
478{
479 into.insert(into.end(), from.begin(), from.end());
480 if (into.empty()) {
481 return;
482 }
483 std::sort(into.begin(), into.end(), [](const Interval& a, const Interval& b) { return a.lo < b.lo; });
484 std::vector<Interval> merged{into.front()};
485 for (size_t i = 1; i < into.size(); ++i) {
486 if (into[i].lo <= merged.back().hi + 1e-9) {
487 merged.back().hi = std::max(merged.back().hi, into[i].hi);
488 } else {
489 merged.push_back(into[i]);
490 }
491 }
492 into.swap(merged);
493}
494
495bool sameStructure(const std::vector<Interval>& a, const std::vector<Interval>& b)
496{
497 if (a.size() != b.size()) {
498 return false;
499 }
500 for (size_t i = 0; i < a.size(); ++i) {
501 if (std::fabs(a[i].lo - b[i].lo) > 0.02 || std::fabs(a[i].hi - b[i].hi) > 0.02) {
502 return false;
503 }
504 }
505 return true;
506}
507
508Support buildSupport(o2::field::MagneticField* field, const std::vector<double>& thresholds, double zmin, double zmax)
509{
510 progress(form(
511 "support: scanning z %.0f..%.0f, dz %.1f cm refined to %.1f, r to %.0f cm, "
512 "phi by arc length <= %.1f cm (%d samples at r=200)",
513 zmin, zmax, kZStepCoarse, kZStepRefine, kScanRMax, kPhiArcStep, phiSamplesAt(200.)));
514
515 std::vector<Slice> slices;
516 Slice previous;
517 bool havePrevious = false;
518 for (double z = zmin; z <= zmax + 1e-9; z += kZStepCoarse) {
519 Slice slice = sliceAt(field, z, thresholds);
520 bool changed = false;
521 for (size_t t = 0; havePrevious && t < thresholds.size(); ++t) {
522 changed = changed || !sameStructure(previous.iv[t], slice.iv[t]);
523 }
524 if (changed) {
525 for (double zz = previous.z + kZStepRefine; zz < z - 1e-9; zz += kZStepRefine) {
526 slices.push_back(sliceAt(field, zz, thresholds));
527 }
528 }
529 slices.push_back(slice);
530 previous = slice;
531 havePrevious = true;
532 if (std::fmod(z - zmin, 500.) < kZStepCoarse / 2) {
533 progress(form("support: ... z = %.0f", z));
534 }
535 }
536
537 for (size_t i = 0; i < slices.size(); ++i) {
538 const double zPrev = (i == 0) ? slices[i].z - kZStepCoarse : slices[i - 1].z;
539 const double zNext = (i + 1 == slices.size()) ? slices[i].z + kZStepCoarse : slices[i + 1].z;
540 slices[i].zlo = 0.5 * (zPrev + slices[i].z);
541 slices[i].zhi = 0.5 * (slices[i].z + zNext);
542 }
543
544 // A band carries the union of the intervals of ITS OWN slices, extended by half
545 // a coarse step at each end so that neighbouring bands overlap and a point
546 // between two samples is claimed by both. Unioning a band with its bracketing
547 // slices instead is sound only for a band one step wide: applied to a merged
548 // band it smears a neighbour's support across the whole length, and the field-
549 // free windows this tool exists to find are precisely empty bands between two
550 // populated ones. Conservatism has to stay local or it destroys them.
551 Support support;
552 support.models.resize(thresholds.size());
553 for (size_t t = 0; t < thresholds.size(); ++t) {
554 support.models[t].thresholdKG = thresholds[t];
555 size_t i = 0;
556 while (i < slices.size()) {
557 size_t j = i;
558 while (j + 1 < slices.size() && sameStructure(slices[j].iv[t], slices[j + 1].iv[t])) {
559 ++j;
560 }
561 Band band;
562 band.zlo = slices[i].zlo - 0.5 * kZStepCoarse;
563 band.zhi = slices[j].zhi + 0.5 * kZStepCoarse;
564 for (size_t k = i; k <= j; ++k) {
565 mergeIntervals(band.iv, slices[k].iv[t]);
566 }
567 if (!band.iv.empty()) {
568 support.models[t].bands.push_back(band);
569 }
570 i = j + 1;
571 }
572 }
573 support.zmin = zmin;
574 support.zmax = zmax;
575 support.rmax = kScanRMax;
576 support.parameterisation = field->getParameterName();
577 return support;
578}
579
586bool violationScan(o2::field::MagneticField* field, const Support& support, Report& report)
587{
588 TRandom3 random(10001);
589 std::vector<long> violations(support.models.size(), 0);
590 std::vector<double> worst(support.models.size(), 0.);
591 std::vector<double> worstR(support.models.size(), 0.);
592 std::vector<double> worstZ(support.models.size(), 0.);
593 for (long i = 0; i < kViolationScanPoints; ++i) {
594 // Half the points anywhere in the domain, half near the axis where the
595 // machine elements are narrow enough to hide between uniform samples.
596 const bool nearAxis = (i % 2 == 1);
597 const double z = nearAxis ? random.Uniform(std::max(support.zmin, -2200.), std::min(support.zmax, 2200.))
598 : random.Uniform(support.zmin, support.zmax);
599 const double r = random.Uniform(0., nearAxis ? 20. : support.rmax);
600 const double b = fieldMagCyl(field, r, random.Uniform(0., 2 * M_PI), z);
601 for (size_t t = 0; t < support.models.size(); ++t) {
602 if (b > support.models[t].thresholdKG && !support.supportAt(t, z, r)) {
603 ++violations[t];
604 if (b > worst[t]) {
605 worst[t] = b;
606 worstR[t] = r;
607 worstZ[t] = z;
608 }
609 }
610 }
611 }
612 bool ok = true;
613 for (size_t t = 0; t < support.models.size(); ++t) {
614 report(form(" outer bound at %6.1f G: %ld / %ld sampled points with field outside every band%s",
615 support.models[t].thresholdKG * 1000., violations[t], kViolationScanPoints,
616 violations[t] == 0 ? " (bound holds)" : " <-- THE MODEL IS NOT AN OUTER BOUND"));
617 if (violations[t] != 0) {
618 report(form(" worst: |B| = %.4f kG at r = %.3f, z = %.3f", worst[t], worstR[t], worstZ[t]));
619 ok = false;
620 }
621 }
622 return ok;
623}
624
625json supportToJson(const Support& support, const std::string& fieldSource)
626{
627 const std::time_t now = std::time(nullptr);
628 char stamp[64];
629 std::strftime(stamp, sizeof(stamp), "%Y-%m-%dT%H:%M:%S", std::gmtime(&now));
630
631 json out;
632 out["schema"] = "o2-sim-geometry-doctor/field_support/1";
633 out["generated_utc"] = stamp;
634 out["field_source"] = fieldSource;
635 out["parameterisation"] = support.parameterisation;
636 out["units"] = "kGauss, cm";
637 out["semantics"] =
638 "Outer bound on the support of |B|, maximised over phi. A point outside every band, "
639 "after expanding intervals by edge_uncertainty_cm, has |B| <= threshold.";
640 out["resolution"] = {{"dz_coarse", kZStepCoarse},
641 {"dz_refine", kZStepRefine},
642 {"dr_near_axis", 0.1},
643 {"phi_arc_step_cm", kPhiArcStep},
644 {"phi_min_samples", kMinPhiSamples},
645 {"bisection_tol_cm", kBisectionTol},
646 {"edge_uncertainty_cm", support.edgeUncertainty}};
647 out["domain"] = {{"zmin", support.zmin}, {"zmax", support.zmax}, {"rmax", support.rmax}};
648 out["recommended_margins_cm"] = {{"strict", support.marginStrict}, {"tight", support.marginTight}};
649 out["models"] = json::array();
650 for (const auto& model : support.models) {
651 json m;
652 m["threshold_kG"] = model.thresholdKG;
653 m["threshold_gauss"] = model.thresholdKG * 1000.;
654 m["n_bands"] = model.bands.size();
655 m["bands"] = json::array();
656 for (const auto& band : model.bands) {
657 json b;
658 b["zlo"] = band.zlo;
659 b["zhi"] = band.zhi;
660 b["iv"] = json::array();
661 for (const auto& iv : band.iv) {
662 b["iv"].push_back(json::array({iv.lo, iv.hi}));
663 }
664 m["bands"].push_back(b);
665 }
666 out["models"].push_back(m);
667 }
668 return out;
669}
670
673bool supportFromJson(const json& in, Support& support)
674{
675 try {
676 support.edgeUncertainty = in.at("resolution").at("edge_uncertainty_cm").get<double>();
677 support.marginStrict = in.at("recommended_margins_cm").at("strict").get<double>();
678 support.marginTight = in.at("recommended_margins_cm").at("tight").get<double>();
679 support.zmin = in.at("domain").at("zmin").get<double>();
680 support.zmax = in.at("domain").at("zmax").get<double>();
681 support.rmax = in.at("domain").at("rmax").get<double>();
682 support.parameterisation = in.value("parameterisation", std::string());
683 for (const auto& m : in.at("models")) {
684 Model model;
685 model.thresholdKG = m.at("threshold_kG").get<double>();
686 for (const auto& b : m.at("bands")) {
687 Band band;
688 band.zlo = b.at("zlo").get<double>();
689 band.zhi = b.at("zhi").get<double>();
690 for (const auto& iv : b.at("iv")) {
691 band.iv.push_back({iv.at(0).get<double>(), iv.at(1).get<double>()});
692 }
693 model.bands.push_back(band);
694 }
695 support.models.push_back(model);
696 }
697 } catch (const std::exception& e) {
698 progress(std::string("error: cannot read the support model: ") + e.what());
699 return false;
700 }
701 if (support.models.empty() || support.models.front().bands.empty()) {
702 progress(
703 "error: the support model is empty -- refusing to continue, since an empty model "
704 "would declare the whole geometry field-free");
705 return false;
706 }
707 return true;
708}
709
710// ---------------------------------------------------------------------------
711// reachability
712// ---------------------------------------------------------------------------
713
735struct Reach {
736 std::string medium, mother, worstPath;
737 long sampled = 0;
738 double fraction = 1.;
739 long ownSampled = 0;
740 double ownFraction = 1.;
741 double vgFraction = 1.;
742 double vgOwnFraction = 1.;
743 long disagreed = 0;
744};
745
746constexpr int kReachRejectionTries = 400;
747
752enum class Navigator { TGeo,
753 VecGeom,
754 Both };
755
759bool insideAnyDaughter(TGeoVolume* volume, const double* local);
760
765struct ReachTask {
766 TGeoNode* node = nullptr;
767 TGeoHMatrix matrix;
768 std::string path;
769 std::vector<TGeoNode*> chain;
770 std::vector<TGeoNode*> flatChain;
771};
772
773struct ReachResult {
774 bool sampled = false;
775 long drawn = 0, reached = 0, ownDrawn = 0, ownReached = 0;
776 long vgReached = 0, vgOwnReached = 0, disagreed = 0;
777};
778
782class ReachCollector
783{
784 public:
785 void walk(TGeoNode* node) { walk(node, TGeoHMatrix(), "", {}, {}); }
786 std::vector<ReachTask>& tasks() { return mTasks; }
787 long nodesVisited() const { return mVisited; }
788
789 private:
790 void walk(TGeoNode* node, const TGeoHMatrix& parent, const std::string& path, std::vector<TGeoNode*> chain,
791 std::vector<TGeoNode*> flatChain);
792 std::set<TGeoNode*> mSeen;
793 std::vector<ReachTask> mTasks;
794 long mVisited = 0;
795};
796
797void ReachCollector::walk(TGeoNode* node, const TGeoHMatrix& parent, const std::string& path,
798 std::vector<TGeoNode*> chain, std::vector<TGeoNode*> flatChain)
799{
800 if (!mSeen.insert(node).second) {
801 return; // this node object, and therefore its whole subtree, is already covered
802 }
803 TGeoHMatrix here = parent;
804 here.Multiply(node->GetMatrix());
805 const std::string myPath = path + "/" + node->GetName();
806 chain.push_back(node);
807 ++mVisited;
808
809 // an assembly is expanded away at closure, so FindNode never returns one
810 if (!node->GetVolume()->IsAssembly()) {
811 flatChain.push_back(node);
812 ReachTask task;
813 task.node = node;
814 task.matrix = here;
815 task.path = myPath;
816 task.chain = chain;
817 task.flatChain = flatChain;
818 mTasks.push_back(std::move(task));
819 }
820 for (int i = 0; i < node->GetNdaughters(); ++i) {
821 walk(node->GetDaughter(i), here, myPath, chain, flatChain);
822 }
823}
824
829bool samplePoint(TGeoShape* shape, TRandom3& random, double* local)
830{
831 auto* box = dynamic_cast<TGeoBBox*>(shape);
832 if (box == nullptr) {
833 return false;
834 }
835 const double* origin = box->GetOrigin();
836 for (int attempt = 0; attempt < kReachRejectionTries; ++attempt) {
837 local[0] = origin[0] + box->GetDX() * (2. * random.Rndm() - 1.);
838 local[1] = origin[1] + box->GetDY() * (2. * random.Rndm() - 1.);
839 local[2] = origin[2] + box->GetDZ() * (2. * random.Rndm() - 1.);
840 if (shape->Contains(local)) {
841 return true;
842 }
843 }
844 return false;
845}
846
852unsigned int seedFor(size_t index)
853{
854 unsigned long long x = 20260901ull + 0x9E3779B97F4A7C15ull * (index + 1);
855 x ^= x >> 30;
856 x *= 0xBF58476D1CE4E5B9ull;
857 x ^= x >> 27;
858 return (unsigned int)(x >> 33) | 1u;
859}
860
865bool tgeoPassesThrough(TGeoNavigator* nav, const std::vector<TGeoNode*>& chain, bool& exact)
866{
867 const int depth = (int)chain.size() - 1;
868 const int level = nav->GetLevel();
869 if (level < depth) {
870 return false;
871 }
872 for (int d = 0; d <= depth; ++d) {
873 if (nav->GetMother(level - d) != chain[d]) {
874 return false;
875 }
876 }
877 exact = (level == depth);
878 return true;
879}
880
887bool sameFlattenedNode(TGeoNode* located, TGeoNode* wanted)
888{
889 if (located == wanted) {
890 return true;
891 }
892 if (located->GetVolume() != wanted->GetVolume()) {
893 return false;
894 }
895 static const std::string kFlattened = "_assemblyinternalcount_";
896 const std::string name = located->GetName(), want = wanted->GetName();
897 return name.size() > want.size() + kFlattened.size() && name.compare(0, want.size(), want) == 0 &&
898 name.compare(want.size(), kFlattened.size(), kFlattened) == 0;
899}
900
905bool vecGeomPassesThrough(const std::vector<TGeoNode*>& located, const std::vector<TGeoNode*>& flatChain, bool& exact)
906{
907 const int depth = (int)flatChain.size() - 1;
908 if ((int)located.size() - 1 < depth) {
909 return false;
910 }
911 for (int d = 0; d <= depth; ++d) {
912 if (!sameFlattenedNode(located[d], flatChain[d])) {
913 return false;
914 }
915 }
916 exact = ((int)located.size() - 1 == depth);
917 return true;
918}
919
920void sampleTask(const ReachTask& task, size_t index, int samples, Navigator backend, TGeoNavigator* nav,
921 std::vector<TGeoNode*>& located, ReachResult& out)
922{
923 TGeoVolume* volume = task.node->GetVolume();
924 const bool hasDaughters = volume->GetNdaughters() > 0;
925 const bool wantTGeo = backend != Navigator::VecGeom;
926 const bool wantVecGeom = backend != Navigator::TGeo;
927 TRandom3 random(seedFor(index));
928 for (int i = 0; i < samples; ++i) {
929 double local[3], global[3];
930 if (!samplePoint(volume->GetShape(), random, local)) {
931 break;
932 }
933 ++out.drawn;
934 // nominally this volume's own material: inside its shape, inside none of its
935 // daughters. A leaf owns every point of its shape, so skip the walk there.
936 const bool own = !hasDaughters || !insideAnyDaughter(volume, local);
937 if (own) {
938 ++out.ownDrawn;
939 }
940 task.matrix.LocalToMaster(local, global);
941
942 bool tgeoThrough = false, tgeoExact = false;
943 if (wantTGeo) {
944 // FindNode() resumes from wherever the navigator currently is, so without
945 // this the audit asks each question from inside the very placement it is
946 // testing and that placement wins every genuinely ambiguous point. Two
947 // mutually overlapping volumes then both report themselves fully reached.
948 // Starting from the top makes the answer the navigator's own, and the same
949 // one a track crossing the region would get.
950 nav->CdTop();
951 if (nav->FindNode(global[0], global[1], global[2]) != nullptr) {
952 tgeoThrough = tgeoPassesThrough(nav, task.chain, tgeoExact);
953 }
954 if (tgeoThrough) {
955 ++out.reached;
956 if (own && tgeoExact) {
957 ++out.ownReached; // it stopped here, so the material really is this one's
958 }
959 }
960 }
961 if (wantVecGeom) {
962 bool vgThrough = false, vgExact = false;
963 if (o2::base::GeometryManager::vecGeomLocate(global[0], global[1], global[2], located)) {
964 vgThrough = vecGeomPassesThrough(located, task.flatChain, vgExact);
965 }
966 if (vgThrough) {
967 ++out.vgReached;
968 if (own && vgExact) {
969 ++out.vgOwnReached;
970 }
971 }
972 if (backend == Navigator::Both && vgThrough != tgeoThrough) {
973 ++out.disagreed;
974 }
975 }
976 }
977 out.sampled = out.drawn > 0;
978}
979
981long reportReachability(int samples, int jobs, Navigator backend, Report& report)
982{
983 if (samples <= 0) {
984 return 0;
985 }
986 progress("reachability: asking the navigator to find every placement from inside its own shape");
987 ReachCollector collector;
988 collector.walk(gGeoManager->GetTopNode());
989 auto& tasks = collector.tasks();
990
991 if (backend != Navigator::TGeo && !o2::base::GeometryManager::ensureVecGeomWorld()) {
992 report(" VecGeom backend requested but this build of O2 has none; falling back to TGeo");
993 backend = Navigator::TGeo;
994 }
995
996 int threads = jobs > 0 ? jobs : (int)std::thread::hardware_concurrency();
997 threads = std::max(1, std::min<int>(threads, (int)tasks.size()));
998 // Every placement is sampled independently, so the only shared state is the
999 // geometry itself. ROOT serves that per thread: SetMaxThreads allocates the
1000 // per-thread shape data (composite shapes and voxel finders cache into it) and
1001 // each worker claims its own navigator, without which they would all drive one.
1002 if (threads > 1) {
1003 gGeoManager->SetMaxThreads(threads);
1004 }
1005 progress(form("reachability: %zu placements, %d point%s each, %d thread%s", tasks.size(), samples,
1006 samples == 1 ? "" : "s", threads, threads == 1 ? "" : "s"));
1007
1008 std::vector<ReachResult> results(tasks.size());
1009 std::atomic<size_t> next{0};
1010 auto worker = [&]() {
1011 TGeoNavigator* nav = threads > 1 ? gGeoManager->AddNavigator() : gGeoManager->GetCurrentNavigator();
1012 std::vector<TGeoNode*> located;
1013 // handed out one at a time: a placement's cost spans orders of magnitude, so a
1014 // static split would leave most threads waiting on the few expensive ones
1015 for (size_t i = next++; i < tasks.size(); i = next++) {
1016 sampleTask(tasks[i], i, samples, backend, nav, located, results[i]);
1017 }
1018 };
1019 if (threads > 1) {
1020 std::vector<std::thread> pool;
1021 pool.reserve(threads);
1022 for (int i = 0; i < threads; ++i) {
1023 pool.emplace_back(worker);
1024 }
1025 for (auto& thread : pool) {
1026 thread.join();
1027 }
1028 } else {
1029 worker();
1030 }
1031
1032 long sampled = 0, unsampleable = 0, disagreeing = 0;
1033 std::vector<Reach> dead, partial;
1034 for (size_t i = 0; i < tasks.size(); ++i) {
1035 const auto& result = results[i];
1036 if (!result.sampled) {
1037 ++unsampleable; // a sliver too thin for the rejection budget; says nothing
1038 continue;
1039 }
1040 ++sampled;
1041 TGeoNode* node = tasks[i].node;
1042 auto* medium = node->GetVolume()->GetMedium();
1043 Reach entry;
1044 entry.medium = medium != nullptr ? medium->GetName() : "(none)";
1045 entry.mother = node->GetMotherVolume() != nullptr ? node->GetMotherVolume()->GetName() : "-";
1046 entry.worstPath = tasks[i].path;
1047 entry.sampled = result.drawn;
1048 entry.ownSampled = result.ownDrawn;
1049 entry.disagreed = result.disagreed;
1050 const bool primaryIsVecGeom = backend == Navigator::VecGeom;
1051 entry.fraction = double(primaryIsVecGeom ? result.vgReached : result.reached) / result.drawn;
1052 entry.ownFraction = result.ownDrawn > 0
1053 ? double(primaryIsVecGeom ? result.vgOwnReached : result.ownReached) / result.ownDrawn
1054 : 1.;
1055 entry.vgFraction = double(result.vgReached) / result.drawn;
1056 entry.vgOwnFraction = result.ownDrawn > 0 ? double(result.vgOwnReached) / result.ownDrawn : 1.;
1057 if (result.disagreed > 0) {
1058 ++disagreeing;
1059 }
1060 // Either number can fail on its own. A mother almost entirely filled by its
1061 // daughters keeps a high reached fraction while the sliver of its own medium
1062 // is taken by a foreign volume, and that sliver is the material that
1063 // disappears -- so classify on whichever of the two is worse.
1064 if (entry.fraction == 0.) {
1065 dead.push_back(entry);
1066 } else if (entry.fraction < 0.999 || entry.ownFraction < 0.999) {
1067 partial.push_back(entry);
1068 }
1069 }
1070
1071 const char* named = backend == Navigator::TGeo ? "TGeo" : (backend == Navigator::VecGeom ? "VecGeom" : "TGeo, cross-checked against VecGeom");
1072 report(form("reachability: %ld node objects visited, %ld sampled, %ld too thin to sample (navigator: %s)",
1073 collector.nodesVisited(), sampled, unsampleable, named));
1074 report(form(" %ld placements the navigator never reaches, %zu it reaches only in part", (long)dead.size(),
1075 partial.size()));
1076 // worst first: with hundreds of small overlaps the walk order is not a ranking
1077 std::sort(partial.begin(), partial.end(), [](const Reach& a, const Reach& b) {
1078 return std::min(a.fraction, a.ownFraction) < std::min(b.fraction, b.ownFraction);
1079 });
1080 if (!dead.empty()) {
1081 report(" unreachable -- these carry no material and produce no hits:");
1082 report(form(" %-12s %-18s %10s %s", "mother", "medium", "sampled", "path"));
1083 for (const auto& entry : dead) {
1084 report(form(" %-12s %-18s %10ld %s", entry.mother.c_str(), entry.medium.c_str(), entry.sampled,
1085 entry.worstPath.c_str()));
1086 }
1087 }
1088 for (size_t i = 0; i < partial.size() && i < 20; ++i) {
1089 const auto& entry = partial[i];
1090 if (i == 0) {
1091 report(" partially shadowed -- an overlapping sibling or an extruding placement.");
1092 report(" 'reached' is how much of the placement the navigator enters at all; 'own kept'");
1093 report(" how much of the medium this volume was given to carry survives as its own:");
1094 report(form(" %-12s %-18s %8s %9s %s", "mother", "medium", "reached", "own kept", "path"));
1095 }
1096 report(form(" %-12s %-18s %7.1f%% %8.1f%% %s", entry.mother.c_str(), entry.medium.c_str(),
1097 100. * entry.fraction, 100. * entry.ownFraction, entry.worstPath.c_str()));
1098 }
1099 if (partial.size() > 20) {
1100 report(form(" ... and %zu more, all above %.1f%%", partial.size() - 20,
1101 100. * std::min(partial[19].fraction, partial[19].ownFraction)));
1102 }
1103
1104 if (backend == Navigator::Both) {
1105 report("");
1106 if (disagreeing == 0) {
1107 report(" TGeo and VecGeom agree on every point sampled.");
1108 } else {
1109 report(form(" %ld placements where the two navigators disagree about who owns a point.", disagreeing));
1110 report(" A disagreement is a real overlap whose resolution depends on the engine, so the");
1111 report(" material a track sees there is not a property of the geometry alone:");
1112 report(form(" %-12s %-18s %8s %8s %8s %s", "mother", "medium", "differ", "TGeo", "VecGeom", "path"));
1113 std::vector<Reach> conflicts;
1114 for (size_t i = 0; i < tasks.size(); ++i) {
1115 if (results[i].disagreed == 0 || !results[i].sampled) {
1116 continue;
1117 }
1118 Reach entry;
1119 TGeoNode* node = tasks[i].node;
1120 auto* medium = node->GetVolume()->GetMedium();
1121 entry.medium = medium != nullptr ? medium->GetName() : "(none)";
1122 entry.mother = node->GetMotherVolume() != nullptr ? node->GetMotherVolume()->GetName() : "-";
1123 entry.worstPath = tasks[i].path;
1124 entry.sampled = results[i].drawn;
1125 entry.disagreed = results[i].disagreed;
1126 entry.fraction = double(results[i].reached) / results[i].drawn;
1127 entry.vgFraction = double(results[i].vgReached) / results[i].drawn;
1128 conflicts.push_back(entry);
1129 }
1130 std::sort(conflicts.begin(), conflicts.end(), [](const Reach& a, const Reach& b) {
1131 return double(a.disagreed) / a.sampled > double(b.disagreed) / b.sampled;
1132 });
1133 for (size_t i = 0; i < conflicts.size() && i < 20; ++i) {
1134 const auto& entry = conflicts[i];
1135 report(form(" %-12s %-18s %7.1f%% %7.1f%% %7.1f%% %s", entry.mother.c_str(), entry.medium.c_str(),
1136 100. * entry.disagreed / entry.sampled, 100. * entry.fraction, 100. * entry.vgFraction,
1137 entry.worstPath.c_str()));
1138 }
1139 if (conflicts.size() > 20) {
1140 report(form(" ... and %zu more", conflicts.size() - 20));
1141 }
1142 }
1143 }
1144 report("");
1145 return (long)dead.size();
1146}
1147
1148// ---------------------------------------------------------------------------
1149// the placement table
1150// ---------------------------------------------------------------------------
1151
1152constexpr int kMaxDepth = 14;
1153constexpr size_t kMaxRows = 400000;
1154constexpr double kSampleDr = 0.05; // cm, radial step of the disproof scan
1155constexpr double kSampleArc = 0.5; // cm, azimuthal step of the disproof scan
1156constexpr double kSampleDzMax = 2.0; // cm
1157constexpr long kMaxSamplesPerRow = 4000000;
1158
1159struct Row {
1160 std::string path, lv, medium, mother, shape;
1161 std::string effectiveMother;
1162 std::string verdict = "UNCLASSIFIED";
1163 int ifield = -1;
1164 int copyNo = 0, nDaughters = 0, depth = 0;
1165 bool sensitive = false, assembly = false, approximateExtent = false, resolved = true;
1166 double zmin = 0., zmax = 0., rmin = 0., rmax = 0.;
1167 double separation = -1., penetration = 0.;
1168 double maxB = -1., minB = -1.;
1169 long nSampled = 0;
1170 bool wholeVolumeSampled = false;
1171 double wholeMaxB = -1., wholeMinB = -1.;
1172 TGeoNode* node = nullptr;
1173 TGeoHMatrix matrix;
1174};
1175
1176bool isOutFamily(const std::string& verdict)
1177{
1178 return verdict == "OUT" || verdict == "OUT_TIGHT" || verdict == "OUT_BOUNDARY";
1179}
1180
1185bool isInFamily(const std::string& verdict)
1186{
1187 return verdict == "IN" || verdict == "IN_COVERED" || verdict == "UNKNOWN" || verdict == "OUTSIDE_DOMAIN";
1188}
1189
1198bool shapeRadii(TGeoShape* shape, double& rmin, double& rmax)
1199{
1200 if (auto* pgon = dynamic_cast<TGeoPgon*>(shape)) {
1201 rmin = 1e30;
1202 rmax = -1e30;
1203 for (int i = 0; i < pgon->GetNz(); ++i) {
1204 rmin = std::min(rmin, pgon->GetRmin(i));
1205 rmax = std::max(rmax, pgon->GetRmax(i));
1206 }
1207 const double edges = pgon->GetNedges() > 2 ? pgon->GetNedges() : 3;
1208 rmax /= std::cos(M_PI / edges);
1209 return true;
1210 }
1211 if (auto* pcon = dynamic_cast<TGeoPcon*>(shape)) {
1212 rmin = 1e30;
1213 rmax = -1e30;
1214 for (int i = 0; i < pcon->GetNz(); ++i) {
1215 rmin = std::min(rmin, pcon->GetRmin(i));
1216 rmax = std::max(rmax, pcon->GetRmax(i));
1217 }
1218 return true;
1219 }
1220 if (auto* cone = dynamic_cast<TGeoCone*>(shape)) {
1221 rmin = std::min(cone->GetRmin1(), cone->GetRmin2());
1222 rmax = std::max(cone->GetRmax1(), cone->GetRmax2());
1223 return true;
1224 }
1225 if (auto* eltu = dynamic_cast<TGeoEltu*>(shape)) {
1226 rmin = 0.; // solid, so it contains the axis
1227 rmax = std::max(eltu->GetA(), eltu->GetB());
1228 return true;
1229 }
1230 if (auto* tube = dynamic_cast<TGeoTube*>(shape)) {
1231 rmin = tube->GetRmin();
1232 rmax = tube->GetRmax();
1233 return true;
1234 }
1235 return false;
1236}
1237
1238bool zPreserving(const TGeoHMatrix& m)
1239{
1240 const Double_t* r = m.GetRotationMatrix();
1241 return std::fabs(std::fabs(r[8]) - 1.) < 1e-9 && std::fabs(r[2]) < 1e-9 && std::fabs(r[5]) < 1e-9 &&
1242 std::fabs(r[6]) < 1e-9 && std::fabs(r[7]) < 1e-9;
1243}
1244
1245void extentOf(TGeoNode* node, const TGeoHMatrix& matrix, Row& row)
1246{
1247 TGeoShape* shape = node->GetVolume()->GetShape();
1248 auto* box = dynamic_cast<TGeoBBox*>(shape);
1249 if (box == nullptr) {
1250 // No bounding box means no analytic extent; claim everything, which classifies
1251 // as in-field and never as OUT.
1252 row.zmin = row.zmax = 0.;
1253 row.rmin = 0.;
1254 row.rmax = 1e9;
1255 row.approximateExtent = true;
1256 return;
1257 }
1258
1259 row.zmin = 1e30;
1260 row.zmax = -1e30;
1261 double boxRmax = 0.;
1262 const double dx = box->GetDX(), dy = box->GetDY(), dz = box->GetDZ();
1263 const double* origin = box->GetOrigin();
1264 for (int i = 0; i < 8; ++i) {
1265 double local[3] = {origin[0] + ((i & 1) ? dx : -dx), origin[1] + ((i & 2) ? dy : -dy),
1266 origin[2] + ((i & 4) ? dz : -dz)};
1267 double global[3];
1268 matrix.LocalToMaster(local, global);
1269 row.zmin = std::min(row.zmin, global[2]);
1270 row.zmax = std::max(row.zmax, global[2]);
1271 boxRmax = std::max(boxRmax, std::hypot(global[0], global[1]));
1272 }
1273
1274 const double* translation = matrix.GetTranslation();
1275 const double offAxis = std::hypot(translation[0] + origin[0], translation[1] + origin[1]);
1276 double localRmin = 0., localRmax = 0.;
1277 if (zPreserving(matrix) && shapeRadii(shape, localRmin, localRmax)) {
1278 row.rmin = std::max(0., localRmin - offAxis);
1279 row.rmax = localRmax + offAxis;
1280 row.approximateExtent = false;
1281 } else {
1282 row.rmax = boxRmax;
1283 row.rmin = (offAxis <= std::hypot(dx, dy)) ? 0. : std::max(0., offAxis - std::hypot(dx, dy));
1284 row.approximateExtent = true;
1285 }
1286}
1287
1288// ---------------------------------------------------------------------------
1289// the doctor
1290// ---------------------------------------------------------------------------
1291
1292struct ContainerProposal {
1293 std::string mother, motherPath;
1294 double zlo, zhi, rmax;
1295 int nDaughters;
1296 bool clearedByStrictMargin;
1297 bool sensitive;
1298};
1299
1300struct SharedVolume {
1301 std::string lv;
1302 std::vector<const Row*> out, in;
1303};
1304
1305class Doctor
1306{
1307 public:
1308 Doctor(o2::field::MagneticField* field, const Support& support) : mField(field), mSupport(support)
1309 {
1310 mThreshold = support.models.front().thresholdKG;
1311 }
1312
1313 void walk(TGeoNode* node) { walk(node, nullptr, TGeoHMatrix(), 0, "", ""); }
1314 void classifyAll();
1315 void findFindings();
1316
1317 const std::vector<Row>& rows() const { return mRows; }
1318 size_t nPruned() const { return mPruned; }
1319 const std::vector<const Row*>& reverseAudit() const { return mReverse; }
1320 const std::vector<SharedVolume>& sharedVolumes() const { return mShared; }
1321 const std::vector<const Row*>& straddlingMothers() const { return mStraddling; }
1322 const std::vector<ContainerProposal>& containers() const { return mContainers; }
1323 bool hasSensitive(TGeoVolume* volume);
1324
1325 private:
1326 void walk(TGeoNode* node, TGeoVolume* mother, const TGeoHMatrix& parent, int depth, const std::string& path,
1327 const std::string& effectiveMother);
1328 void classify(Row& row);
1329 void disproofScan(Row& row);
1330 void wholeVolumeScan(Row& row);
1331 bool ownMaterialAt(const Row& row, const double* global) const;
1332
1334 const Support& mSupport;
1335 double mThreshold;
1336 std::vector<Row> mRows;
1337 std::map<TGeoVolume*, int> mSensitiveCache;
1338 size_t mPruned = 0;
1339
1340 std::vector<const Row*> mReverse;
1341 std::vector<SharedVolume> mShared;
1342 std::vector<const Row*> mStraddling;
1343 std::vector<ContainerProposal> mContainers;
1344};
1345
1346bool Doctor::hasSensitive(TGeoVolume* volume)
1347{
1348 auto cached = mSensitiveCache.find(volume);
1349 if (cached != mSensitiveCache.end() && cached->second >= 0) {
1350 return cached->second == 1;
1351 }
1352 mSensitiveCache[volume] = 0; // guard against re-entry
1353 auto* medium = volume->GetMedium();
1354 bool found = medium != nullptr && medium->GetParam(0) != 0.;
1355 for (int i = 0; i < volume->GetNdaughters() && !found; ++i) {
1356 found = hasSensitive(volume->GetNode(i)->GetVolume());
1357 }
1358 mSensitiveCache[volume] = found ? 1 : 0;
1359 return found;
1360}
1361
1368bool isStructural(TGeoNode* node, TGeoVolume* mother)
1369{
1370 if (mother == nullptr) {
1371 return true;
1372 }
1373 TGeoVolume* volume = node->GetVolume();
1374 if (volume->IsAssembly()) {
1375 return true;
1376 }
1377 auto* mine = volume->GetMedium();
1378 auto* theirs = mother->GetMedium();
1379 return mine != nullptr && theirs != nullptr && std::strcmp(mine->GetName(), theirs->GetName()) == 0;
1380}
1381
1382void Doctor::walk(TGeoNode* node, TGeoVolume* mother, const TGeoHMatrix& parent, int depth, const std::string& path,
1383 const std::string& effectiveMother)
1384{
1385 if (mRows.size() >= kMaxRows || depth > kMaxDepth) {
1386 return;
1387 }
1388 TGeoHMatrix here = parent;
1389 here.Multiply(node->GetMatrix());
1390 TGeoVolume* volume = node->GetVolume();
1391 const std::string myPath = path + "/" + volume->GetName() + "_" + std::to_string(node->GetNumber());
1392
1393 Row row;
1394 row.path = myPath;
1395 row.lv = volume->GetName();
1396 row.mother = mother != nullptr ? mother->GetName() : "";
1397 row.effectiveMother = effectiveMother;
1398 row.shape = volume->GetShape()->ClassName();
1399 row.copyNo = node->GetNumber();
1400 row.nDaughters = volume->GetNdaughters();
1401 row.depth = depth;
1402 row.assembly = volume->IsAssembly();
1403 auto* medium = volume->GetMedium();
1404 row.medium = medium != nullptr ? medium->GetName() : "(none)";
1405 row.ifield = medium != nullptr ? (int)medium->GetParam(1) : -1;
1406 row.sensitive = medium != nullptr && medium->GetParam(0) != 0.;
1407 row.node = node;
1408 row.matrix = here;
1409 extentOf(node, here, row);
1410 mRows.push_back(row);
1411
1412 if (hasSensitive(volume) && !isStructural(node, mother)) {
1413 ++mPruned;
1414 return;
1415 }
1416 for (int i = 0; i < node->GetNdaughters(); ++i) {
1417 walk(node->GetDaughter(i), volume, here, depth + 1, myPath, row.assembly ? effectiveMother : myPath);
1418 }
1419}
1420
1421// A mother's shape includes the space its daughters occupy, but its MATERIAL is
1422// only what they leave over. That distinction is the whole point of these scans:
1423// caveRB24's shape reaches the beam axis, and what its daughters leave over is a
1424// sliver of cave air at r ~ 3-4 cm, between an oval beam pipe and the circular
1425// field cylinder, carrying 13.2 kG.
1426bool insideAnyDaughter(TGeoVolume* volume, const double* local)
1427{
1428 for (int i = 0; i < volume->GetNdaughters(); ++i) {
1429 TGeoNode* daughter = volume->GetNode(i);
1430 double inDaughter[3];
1431 daughter->GetMatrix()->MasterToLocal(local, inDaughter);
1432 if (!daughter->GetVolume()->GetShape()->Contains(inDaughter)) {
1433 continue;
1434 }
1435 if (daughter->GetVolume()->IsAssembly()) {
1436 if (insideAnyDaughter(daughter->GetVolume(), inDaughter)) {
1437 return true;
1438 }
1439 } else {
1440 return true;
1441 }
1442 }
1443 return false;
1444}
1445
1446bool Doctor::ownMaterialAt(const Row& row, const double* global) const
1447{
1448 double local[3];
1449 row.matrix.MasterToLocal(global, local);
1450 if (!row.node->GetVolume()->GetShape()->Contains(local)) {
1451 return false;
1452 }
1453 return !insideAnyDaughter(row.node->GetVolume(), local);
1454}
1455
1456double thinnestDaughter(TGeoVolume* volume)
1457{
1458 double thinnest = 1e30;
1459 for (int i = 0; i < volume->GetNdaughters(); ++i) {
1460 auto* box = dynamic_cast<TGeoBBox*>(volume->GetNode(i)->GetVolume()->GetShape());
1461 if (box != nullptr) {
1462 thinnest = std::min(thinnest, 2 * std::min(box->GetDX(), std::min(box->GetDY(), box->GetDZ())));
1463 }
1464 }
1465 return thinnest;
1466}
1467
1471void Doctor::disproofScan(Row& row)
1472{
1473 const double thinnest = thinnestDaughter(row.node->GetVolume());
1474 row.resolved = (row.nDaughters == 0) || (kSampleDr <= std::max(0.1, thinnest));
1475 long budget = kMaxSamplesPerRow;
1476 for (const auto& band : mSupport.models.front().bands) {
1477 const double z0 = std::max(row.zmin, band.zlo);
1478 const double z1 = std::min(row.zmax, band.zhi);
1479 if (z1 < z0) {
1480 continue;
1481 }
1482 for (const auto& iv : band.iv) {
1483 const double r0 = std::max(row.rmin, iv.lo - mSupport.edgeUncertainty);
1484 const double r1 = std::min(row.rmax, iv.hi + mSupport.edgeUncertainty);
1485 if (r1 < r0) {
1486 continue;
1487 }
1488 const double dz = std::min(kSampleDzMax, std::max(0.25, (z1 - z0) / 200.));
1489 for (double z = z0; z <= z1 + 1e-9 && budget > 0; z += dz) {
1490 for (double r = r0; r <= r1 + 1e-9 && budget > 0; r += kSampleDr) {
1491 const int nphi = (r <= 0.) ? 1 : std::max(8, (int)std::ceil(2 * M_PI * r / kSampleArc));
1492 for (int i = 0; i < nphi && budget > 0; ++i) {
1493 const double phi = 2 * M_PI * i / nphi;
1494 double global[3] = {r * std::cos(phi), r * std::sin(phi), z};
1495 --budget;
1496 if (!ownMaterialAt(row, global)) {
1497 continue;
1498 }
1499 ++row.nSampled;
1500 const double b = fieldMag(mField, global[0], global[1], global[2]);
1501 row.maxB = std::max(row.maxB, b);
1502 row.minB = (row.minB < 0.) ? b : std::min(row.minB, b);
1503 }
1504 }
1505 }
1506 }
1507 }
1508 if (budget <= 0) {
1509 row.resolved = false;
1510 }
1511}
1512
1516void Doctor::wholeVolumeScan(Row& row)
1517{
1518 row.wholeVolumeSampled = true;
1519 const double dz = std::max(1.0, (row.zmax - row.zmin) / 300.);
1520 const double dr = std::max(0.5, (row.rmax - row.rmin) / 200.);
1521 long budget = 2000000;
1522 for (double z = row.zmin; z <= row.zmax && budget > 0; z += dz) {
1523 for (double r = row.rmin; r <= row.rmax && budget > 0; r += dr) {
1524 const int nphi = (r <= 0.) ? 1 : std::max(8, (int)std::ceil(2 * M_PI * r / std::max(2.0, dr)));
1525 for (int i = 0; i < nphi && budget > 0; ++i) {
1526 const double phi = 2 * M_PI * i / nphi;
1527 double global[3] = {r * std::cos(phi), r * std::sin(phi), z};
1528 --budget;
1529 if (!ownMaterialAt(row, global)) {
1530 continue;
1531 }
1532 const double b = fieldMag(mField, global[0], global[1], global[2]);
1533 row.wholeMaxB = std::max(row.wholeMaxB, b);
1534 row.wholeMinB = (row.wholeMinB < 0.) ? b : std::min(row.wholeMinB, b);
1535 }
1536 }
1537 }
1538}
1539
1540void Doctor::classify(Row& row)
1541{
1542 if (row.assembly) {
1543 row.verdict = "ASSEMBLY"; // virtual: no material, so nothing to flag
1544 return;
1545 }
1546 if (!mSupport.inDomain(row.zmin, row.zmax, row.rmax)) {
1547 row.verdict = "OUTSIDE_DOMAIN";
1548 return;
1549 }
1550 row.separation = mSupport.separation(0, row.zmin, row.zmax, row.rmin, row.rmax);
1551 if (row.separation >= mSupport.marginStrict) {
1552 row.verdict = "OUT";
1553 return;
1554 }
1555 if (row.separation >= mSupport.marginTight) {
1556 row.verdict = "OUT_TIGHT";
1557 return;
1558 }
1559 row.penetration = mSupport.penetration(0, row.zmin, row.zmax, row.rmin, row.rmax);
1560 if (mSupport.coveredBySupport(0, row.zmin, row.zmax, row.rmin, row.rmax)) {
1561 row.verdict = "IN_COVERED"; // in the field by containment, nothing to disprove
1562 return;
1563 }
1564 disproofScan(row);
1565 if (row.maxB > mThreshold) {
1566 row.verdict = "IN";
1567 } else if (row.separation > 0. || row.penetration <= 2 * mSupport.edgeUncertainty) {
1568 // Touches the support only to within what the model can resolve. Field-freeness
1569 // then rests on an analytic claim about a hard boundary, so it is emitted for a
1570 // human to confirm and never treated as established.
1571 row.verdict = "OUT_BOUNDARY";
1572 } else {
1573 row.verdict = "UNKNOWN";
1574 }
1575}
1576
1577void Doctor::classifyAll()
1578{
1579 size_t done = 0;
1580 for (auto& row : mRows) {
1581 classify(row);
1582 if (++done % 50000 == 0) {
1583 progress(form("classify: %zu / %zu placements", done, mRows.size()));
1584 }
1585 }
1586 for (auto& row : mRows) {
1587 // Only a straddler can be heterogeneous: a placement wholly inside the support
1588 // is uniformly in the field, one that is OUT is uniformly out of it.
1589 if (row.nDaughters > 0 && !row.assembly &&
1590 (row.verdict == "IN" || row.verdict == "UNKNOWN" || row.verdict == "OUT_BOUNDARY")) {
1591 wholeVolumeScan(row);
1592 }
1593 }
1594}
1595
1596void Doctor::findFindings()
1597{
1598 // --- the reverse audit: ifield == 0 media that reach into the field ---------
1599 // An ifield == 0 medium tells the transport engine to move in a straight line.
1600 // Where that is wrong it is a physics bug, so the finding must carry the field
1601 // actually present in the volume's own material, measured. Rows short-circuited
1602 // as IN_COVERED were never sampled, so they are sampled now.
1603 for (auto& row : mRows) {
1604 if (row.assembly || row.ifield != 0 || row.medium == "dummy" || row.medium == "(none)") {
1605 continue;
1606 }
1607 if (isInFamily(row.verdict)) {
1608 if (!row.wholeVolumeSampled) {
1609 wholeVolumeScan(row);
1610 }
1611 mReverse.push_back(&row);
1612 }
1613 }
1614
1615 // --- shared logical volumes placed on both sides (signature C) --------------
1616 std::map<std::string, std::vector<const Row*>> byVolume;
1617 for (const auto& row : mRows) {
1618 if (!row.assembly) {
1619 byVolume[row.lv].push_back(&row);
1620 }
1621 }
1622 for (const auto& entry : byVolume) {
1623 SharedVolume shared;
1624 shared.lv = entry.first;
1625 for (const auto* row : entry.second) {
1626 if (isOutFamily(row->verdict)) {
1627 shared.out.push_back(row);
1628 } else if (isInFamily(row->verdict)) {
1629 shared.in.push_back(row);
1630 }
1631 }
1632 if (!shared.out.empty() && !shared.in.empty()) {
1633 mShared.push_back(shared);
1634 }
1635 }
1636 std::sort(mShared.begin(), mShared.end(),
1637 [](const SharedVolume& a, const SharedVolume& b) { return a.out.size() > b.out.size(); });
1638
1639 // --- mothers whose own material straddles the predicate (signature D) -------
1640 for (const auto& row : mRows) {
1641 if (row.nDaughters > 0 && !row.assembly && row.wholeVolumeSampled && row.wholeMaxB > mThreshold &&
1642 row.wholeMinB <= mThreshold) {
1643 mStraddling.push_back(&row);
1644 }
1645 }
1646
1647 // --- missing containers -----------------------------------------------------
1648 // Where a container belongs is not where the daughters have a spatial gap: a
1649 // beam pipe is a continuous chain of volumes with no gap wider than a flange, so
1650 // gap-based clustering returns one cluster spanning everything and proposes
1651 // nothing. What separates the daughters is the PREDICATE. A maximal run of
1652 // consecutive field-free daughters is exactly a group that cannot be given its
1653 // own medium today, because the only thing enclosing it is the mother's air,
1654 // which is not field-free along its whole length.
1655 for (const auto* mother : mStraddling) {
1656 std::vector<const Row*> kids;
1657 for (const auto& row : mRows) {
1658 if (row.effectiveMother == mother->path && !row.assembly) {
1659 kids.push_back(&row);
1660 }
1661 }
1662 std::sort(kids.begin(), kids.end(), [](const Row* a, const Row* b) { return a->zmin < b->zmin; });
1663
1664 size_t i = 0;
1665 while (i < kids.size()) {
1666 if (!isOutFamily(kids[i]->verdict)) {
1667 ++i;
1668 continue;
1669 }
1670 size_t j = i;
1671 double zlo = kids[i]->zmin, zhi = kids[i]->zmax, rmax = kids[i]->rmax;
1672 while (j + 1 < kids.size() && isOutFamily(kids[j + 1]->verdict)) {
1673 ++j;
1674 zlo = std::min(zlo, kids[j]->zmin);
1675 zhi = std::max(zhi, kids[j]->zmax);
1676 rmax = std::max(rmax, kids[j]->rmax);
1677 }
1678 const int n = (int)(j - i + 1);
1679
1680 // A container must not swallow anything outside its run. Rejecting any run
1681 // with an intruder is too blunt: a 0.05 cm overhang from a neighbouring pipe
1682 // section would kill a 51-daughter proposal. The right answer to a 0.05 cm
1683 // overhang is to move the container's edge, so the edges are clamped past any
1684 // overhanging neighbour and only a residual overlap makes the run unusable.
1685 double clampedLo = zlo, clampedHi = zhi;
1686 std::vector<const Row*> intruders;
1687 for (size_t k = 0; k < kids.size(); ++k) {
1688 if (k >= i && k <= j) {
1689 continue;
1690 }
1691 const Row* other = kids[k];
1692 if (other->zmax <= zlo + 0.01 || other->zmin >= zhi - 0.01 || other->rmin >= rmax - 0.01) {
1693 continue;
1694 }
1695 intruders.push_back(other);
1696 }
1697 for (const Row* other : intruders) {
1698 if (other->zmin <= clampedLo + 1e-9 && other->zmax > clampedLo) {
1699 clampedLo = other->zmax;
1700 }
1701 if (other->zmax >= clampedHi - 1e-9 && other->zmin < clampedHi) {
1702 clampedHi = other->zmin;
1703 }
1704 }
1705 bool residual = false;
1706 for (const Row* other : intruders) {
1707 residual = residual || (other->zmax > clampedLo + 0.01 && other->zmin < clampedHi - 0.01 &&
1708 other->rmin < rmax - 0.01);
1709 }
1710
1711 const double separation =
1712 (clampedHi > clampedLo) ? mSupport.separation(0, clampedLo, clampedHi, 0., rmax) : -1.;
1713 if (n >= 2 && separation >= mSupport.marginTight && !residual) {
1714 ContainerProposal proposal;
1715 proposal.mother = mother->lv;
1716 proposal.motherPath = mother->path;
1717 proposal.zlo = clampedLo;
1718 proposal.zhi = clampedHi;
1719 proposal.rmax = rmax;
1720 proposal.nDaughters = n;
1721 proposal.clearedByStrictMargin = separation >= mSupport.marginStrict;
1722 proposal.sensitive = false;
1723 for (size_t k = i; k <= j; ++k) {
1724 proposal.sensitive = proposal.sensitive || hasSensitive(kids[k]->node->GetVolume());
1725 }
1726 mContainers.push_back(proposal);
1727 }
1728 i = j + 1;
1729 }
1730 }
1731}
1732
1733// ---------------------------------------------------------------------------
1734// outputs
1735// ---------------------------------------------------------------------------
1736
1737void writePlacementCsv(const std::vector<Row>& rows, const std::string& path)
1738{
1739 std::FILE* out = std::fopen(path.c_str(), "w");
1740 if (out == nullptr) {
1741 progress("error: cannot write " + path);
1742 return;
1743 }
1744 std::fprintf(out,
1745 "path,lv,medium,ifield,shape,mother,copy,ndaughters,sensitive,assembly,approx,"
1746 "zmin,zmax,rmin,rmax,verdict,separation_cm,penetration_cm,maxB_kG,nsampled\n");
1747 for (const auto& row : rows) {
1748 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(),
1749 row.lv.c_str(), row.medium.c_str(), row.ifield, row.shape.c_str(), row.mother.c_str(), row.copyNo,
1750 row.nDaughters, (int)row.sensitive, (int)row.assembly, (int)row.approximateExtent, row.zmin, row.zmax,
1751 row.rmin, row.rmax, row.verdict.c_str(), row.separation, row.penetration, row.maxB, row.nSampled);
1752 }
1753 std::fclose(out);
1754}
1755
1756json placementJson(const Row& row)
1757{
1758 return json{{"path", row.path},
1759 {"copy", row.copyNo},
1760 {"z", json::array({row.zmin, row.zmax})},
1761 {"r", json::array({row.rmin, row.rmax})},
1762 {"verdict", row.verdict},
1763 {"separation_cm", row.separation},
1764 {"max_B_kG", std::max(row.maxB, row.wholeMaxB)}};
1765}
1766
1767json proposalsToJson(Doctor& doctor, const Support& support, const std::string& geometryFile,
1768 const std::string& fieldSource)
1769{
1770 const std::time_t now = std::time(nullptr);
1771 char stamp[64];
1772 std::strftime(stamp, sizeof(stamp), "%Y-%m-%dT%H:%M:%S", std::gmtime(&now));
1773
1774 json out;
1775 out["schema"] = "o2-sim-geometry-doctor/proposals/1";
1776 out["generated_utc"] = stamp;
1777 out["geometry"] = geometryFile;
1778 out["field_source"] = fieldSource;
1779 out["threshold_kG"] = support.models.front().thresholdKG;
1780 out["margins_cm"] = {{"strict", support.marginStrict},
1781 {"tight", support.marginTight},
1782 {"edge_uncertainty", support.edgeUncertainty}};
1783 out["proposals"] = json::array();
1784
1785 for (const auto& shared : doctor.sharedVolumes()) {
1786 bool refused = false;
1787 for (const auto* row : shared.out) {
1788 refused = refused || row->sensitive || doctor.hasSensitive(row->node->GetVolume());
1789 }
1790 json entry;
1791 entry["signature"] = "shared-volume";
1792 entry["action"] = "split the logical volume, so that its field-free placements can carry a field-free medium";
1793 entry["logical_volume"] = shared.lv;
1794 entry["n_out"] = shared.out.size();
1795 entry["n_in"] = shared.in.size();
1796 entry["status"] = refused ? "refused by default (sensitive path)" : "proposed";
1797 entry["out_placements"] = json::array();
1798 for (const auto* row : shared.out) {
1799 entry["out_placements"].push_back(placementJson(*row));
1800 }
1801 entry["in_placements"] = json::array();
1802 for (const auto* row : shared.in) {
1803 entry["in_placements"].push_back(placementJson(*row));
1804 }
1805 out["proposals"].push_back(entry);
1806 }
1807
1808 for (const auto* row : doctor.reverseAudit()) {
1809 const double maxB = std::max(row->maxB, row->wholeMaxB);
1810 json entry;
1811 entry["signature"] = "reverse-audit";
1812 entry["action"] = maxB > support.models.front().thresholdKG
1813 ? "the field-free medium assignment is wrong: straight-line transport inside real field"
1814 : "the field-free medium reaches field support but no field was found in its own material, review";
1815 entry["path"] = row->path;
1816 entry["logical_volume"] = row->lv;
1817 entry["medium"] = row->medium;
1818 entry["copy"] = row->copyNo;
1819 entry["z"] = json::array({row->zmin, row->zmax});
1820 entry["r"] = json::array({row->rmin, row->rmax});
1821 entry["min_B_kG"] = row->wholeMinB;
1822 entry["max_B_kG"] = maxB;
1823 entry["verdict"] = row->verdict;
1824 out["proposals"].push_back(entry);
1825 }
1826
1827 for (const auto& container : doctor.containers()) {
1828 json entry;
1829 entry["signature"] = "missing-container";
1830 entry["action"] = "insert a container with a field-free medium and re-parent the cluster into it";
1831 entry["mother"] = container.mother;
1832 entry["mother_path"] = container.motherPath;
1833 entry["z"] = json::array({container.zlo, container.zhi});
1834 entry["rmax"] = container.rmax;
1835 entry["n_daughters"] = container.nDaughters;
1836 entry["status"] = container.sensitive ? "refused by default (sensitive path)" : "proposed";
1837 entry["clearance"] = container.clearedByStrictMargin ? "strict margin" : "tight margin";
1838 out["proposals"].push_back(entry);
1839 }
1840
1841 for (const auto* row : doctor.straddlingMothers()) {
1842 json entry;
1843 entry["signature"] = "heterogeneous-mother";
1844 entry["action"] =
1845 "the mother's own material spans both sides of the predicate, so no per-medium flag can "
1846 "express it; it needs a container";
1847 entry["path"] = row->path;
1848 entry["logical_volume"] = row->lv;
1849 entry["medium"] = row->medium;
1850 entry["min_B_kG"] = row->wholeMinB;
1851 entry["max_B_kG"] = row->wholeMaxB;
1852 entry["n_daughters"] = row->nDaughters;
1853 out["proposals"].push_back(entry);
1854 }
1855 return out;
1856}
1857
1858// ---------------------------------------------------------------------------
1859// the anchor self-check
1860// ---------------------------------------------------------------------------
1861
1862// A unit test for this tool would need a placed ALICE geometry and a field map,
1863// neither of which belongs in the repository, so the regression gate is instead a
1864// file of expectations that any ALICE Run 3 geometry must satisfy, checked against
1865// a real run with --verify-anchors. See run/geometry-doctor-anchors.json.
1866//
1867// Supported expectations: OUT (every placement of the volume is in the OUT
1868// family), IN, NOT_OUT, ASSEMBLY, and REVERSE_AUDIT_FLAGGED. An anchor may also
1869// require a placement count and a lower bound on the field found in the volume's
1870// own material.
1871
1872bool verifyAnchors(const std::string& path, Doctor& doctor, Report& report)
1873{
1874 std::ifstream in(path);
1875 if (!in) {
1876 report(" cannot open the anchor file " + path);
1877 return false;
1878 }
1879 json anchors;
1880 try {
1881 in >> anchors;
1882 } catch (const std::exception& e) {
1883 report(std::string(" cannot parse the anchor file: ") + e.what());
1884 return false;
1885 }
1886
1887 std::set<std::string> flaggedByReverseAudit;
1888 for (const auto* row : doctor.reverseAudit()) {
1889 flaggedByReverseAudit.insert(row->lv);
1890 }
1891
1892 bool allPassed = true;
1893 report(form(" %-22s %-22s %-10s %s", "volume", "expected", "verdict", "evidence"));
1894 for (const auto& anchor : anchors.at("anchors")) {
1895 const auto volume = anchor.at("volume").get<std::string>();
1896 const auto expected = anchor.at("expect").get<std::string>();
1897
1898 int placements = 0;
1899 int failures = 0;
1900 double worstSeparation = 1e30;
1901 double bestField = -1.;
1902 std::string reported;
1903 for (const auto& row : doctor.rows()) {
1904 if (row.lv != volume) {
1905 continue;
1906 }
1907 ++placements;
1908 bool ok = true;
1909 if (expected == "OUT") {
1910 ok = isOutFamily(row.verdict) || row.verdict == "ASSEMBLY";
1911 } else if (expected == "IN") {
1912 ok = row.verdict == "IN";
1913 } else if (expected == "NOT_OUT") {
1914 ok = !isOutFamily(row.verdict);
1915 } else if (expected == "ASSEMBLY") {
1916 ok = row.verdict == "ASSEMBLY";
1917 } else if (expected == "REVERSE_AUDIT_FLAGGED") {
1918 ok = true; // decided below, on the volume rather than the placement
1919 } else {
1920 report(" unknown expectation '" + expected + "' for " + volume);
1921 return false;
1922 }
1923 failures += ok ? 0 : 1;
1924 bestField = std::max(bestField, std::max(row.maxB, row.wholeMaxB));
1925 if (row.separation < worstSeparation) {
1926 worstSeparation = row.separation;
1927 reported = row.verdict;
1928 }
1929 }
1930
1931 if (expected == "REVERSE_AUDIT_FLAGGED") {
1932 failures = flaggedByReverseAudit.count(volume) > 0 ? 0 : 1;
1933 reported = failures == 0 ? "flagged" : "not flagged";
1934 }
1935 if (placements == 0) {
1936 failures = 1;
1937 reported = "not placed";
1938 }
1939 if (anchor.contains("placements") && placements != anchor.at("placements").get<int>()) {
1940 failures += 1;
1941 }
1942 if (anchor.contains("min_max_B_kG") && bestField < anchor.at("min_max_B_kG").get<double>()) {
1943 failures += 1;
1944 }
1945
1946 allPassed = allPassed && failures == 0;
1947 report(form(" %-22s %-22s %-10s %d placement(s), separation %.3f cm, max|B| %.4f kG %s", volume.c_str(),
1948 expected.c_str(), reported.c_str(), placements, worstSeparation > 1e29 ? -1. : worstSeparation,
1949 bestField, failures == 0 ? "PASS" : "FAIL"));
1950 }
1951 return allPassed;
1952}
1953
1954// ---------------------------------------------------------------------------
1955
1956struct Options {
1957 std::string geometryFile;
1958 std::string fieldFile;
1959 int fieldCurrent = 0;
1960 std::string supportFile;
1961 std::string anchorFile;
1962 std::vector<double> thresholdsGauss;
1963 double margin = 5.0;
1964 int reachSamples = 32;
1965 int reachJobs = 0;
1966 std::string navigator = "tgeo";
1967 bool reachabilityOnly = false;
1968 std::string outputPrefix = "geometry-doctor";
1969};
1970
1971} // namespace
1972
1973int main(int argc, char** argv)
1974{
1975 Options options;
1976 bpo::options_description description(
1977 "Audits a placed geometry against the magnetic field it will be transported "
1978 "in, and reports where the two do not fit together.\n\nOptions");
1979 description.add_options() //
1980 ("help,h", "print this help message") //
1981 ("geometry-file", bpo::value<std::string>(&options.geometryFile)->required(), //
1982 "the geometry to audit, e.g. o2sim_geometry.root") //
1983 ("field-file", bpo::value<std::string>(&options.fieldFile), //
1984 "a serialized MagneticField carrying reference probe vectors") //
1985 ("field-current", bpo::value<int>(&options.fieldCurrent), //
1986 "build the nominal field for this L3 current instead, e.g. -5") //
1987 ("support-file", bpo::value<std::string>(&options.supportFile), //
1988 "field-support model cache: read it if it exists, otherwise write it") //
1989 ("threshold", bpo::value<std::vector<double>>(&options.thresholdsGauss)->composing(), //
1990 "field threshold in Gauss, repeatable; the lowest one decides the verdicts (default 1 and 10)") //
1991 ("margin", bpo::value<double>(&options.margin)->default_value(5.0), //
1992 "clearance in cm a placement must keep from the field support to be called field-free") //
1993 ("output-prefix", bpo::value<std::string>(&options.outputPrefix)->default_value("geometry-doctor"), //
1994 "prefix for the report, the proposals and the placement table") //
1995 ("verify-anchors", bpo::value<std::string>(&options.anchorFile), //
1996 "check the classification against known-good volumes listed in this JSON file") //
1997 ("reachability-samples", bpo::value<int>(&options.reachSamples)->default_value(1000), //
1998 "points drawn inside each placement for the reachability audit; 0 disables it. Below a few " //
1999 "hundred the audit reports genuine placements as partially shadowed") //
2000 ("reachability-jobs", bpo::value<int>(&options.reachJobs)->default_value(0), //
2001 "threads for the reachability audit; 0 uses every core. The answer does not depend on it") //
2002 ("navigator", bpo::value<std::string>(&options.navigator)->default_value("tgeo"), //
2003 "which navigator answers 'what is at this point': tgeo, vecgeom, or both. 'both' reports where " //
2004 "they disagree, which is where a real overlap is resolved differently by the two engines") //
2005 ("reachability-only", bpo::bool_switch(&options.reachabilityOnly), //
2006 "run only the reachability audit, which needs no magnetic field");
2007
2008 bpo::variables_map arguments;
2009 try {
2010 bpo::store(bpo::parse_command_line(argc, argv, description), arguments);
2011 if (arguments.count("help") != 0u) {
2012 std::cout << description << '\n';
2013 return 0;
2014 }
2015 bpo::notify(arguments);
2016 } catch (const bpo::error& e) {
2017 std::cerr << "error: " << e.what() << "\n\n"
2018 << description << '\n';
2019 return 1;
2020 }
2021
2022 Navigator navigator = Navigator::TGeo;
2023 if (options.navigator == "vecgeom") {
2024 navigator = Navigator::VecGeom;
2025 } else if (options.navigator == "both") {
2026 navigator = Navigator::Both;
2027 } else if (options.navigator != "tgeo") {
2028 std::cerr << "error: --navigator takes tgeo, vecgeom or both\n";
2029 return 1;
2030 }
2031
2032 const bool haveFieldFile = arguments.count("field-file") != 0u;
2033 const bool haveFieldCurrent = arguments.count("field-current") != 0u;
2034
2035 // The reachability audit is a question about the geometry alone, so it is the one
2036 // part of this tool that can run without a field.
2037 if (options.reachabilityOnly) {
2038 TGeoManager::Import(options.geometryFile.c_str());
2039 if (gGeoManager == nullptr) {
2040 std::cerr << "error: no TGeoManager in " << options.geometryFile << '\n';
2041 return 1;
2042 }
2043 Report report;
2044 report("ALICE simulation geometry doctor -- reachability audit");
2045 report("");
2046 report(" geometry : " + options.geometryFile);
2047 report(form(" volumes : %d, media %d", gGeoManager->GetListOfVolumes()->GetEntries(),
2048 gGeoManager->GetListOfMedia()->GetEntries()));
2049 report("");
2050 const long dead = reportReachability(options.reachSamples, options.reachJobs, navigator, report);
2051 const std::string reportPath = options.outputPrefix + "-report.txt";
2052 report("wrote " + reportPath);
2053 report.write(reportPath);
2054 return dead == 0 ? 0 : 3;
2055 }
2056
2057 if (haveFieldFile == haveFieldCurrent) {
2058 std::cerr << "error: give exactly one of --field-file and --field-current\n";
2059 return 1;
2060 }
2061 if (options.thresholdsGauss.empty()) {
2062 options.thresholdsGauss = {1., 10.};
2063 }
2064 std::sort(options.thresholdsGauss.begin(), options.thresholdsGauss.end());
2065 std::vector<double> thresholds; // kGauss, as the field itself reports
2066 for (double gauss : options.thresholdsGauss) {
2067 thresholds.push_back(gauss * 1e-3);
2068 }
2069
2070 const std::string fieldSource =
2071 haveFieldFile ? options.fieldFile : form("createNominalField(%d)", options.fieldCurrent);
2073 haveFieldFile ? loadFieldFromFile(options.fieldFile) : o2::field::MagneticField::createNominalField(options.fieldCurrent);
2074 if (field == nullptr) {
2075 std::cerr << "error: no usable magnetic field\n";
2076 return 1;
2077 }
2078
2079 Report report;
2080 report("ALICE simulation geometry doctor");
2081 report("");
2082 report(" geometry : " + options.geometryFile);
2083 report(" field : " + fieldSource + ", parameterisation " + field->getParameterName());
2084
2085 // --- the field-support model ------------------------------------------------
2086 Support support;
2087 bool supportFromCache = false;
2088 if (!options.supportFile.empty()) {
2089 std::ifstream cache(options.supportFile);
2090 if (cache) {
2091 json cached;
2092 try {
2093 cache >> cached;
2094 } catch (const std::exception& e) {
2095 std::cerr << "error: cannot parse " << options.supportFile << ": " << e.what() << '\n';
2096 return 1;
2097 }
2098 if (!supportFromJson(cached, support)) {
2099 return 1;
2100 }
2101 supportFromCache = true;
2102 }
2103 }
2104
2105 if (supportFromCache) {
2106 if (support.models.size() != thresholds.size()) {
2107 std::cerr << "error: " << options.supportFile << " carries " << support.models.size()
2108 << " thresholds but " << thresholds.size() << " were requested\n";
2109 return 1;
2110 }
2111 for (size_t t = 0; t < thresholds.size(); ++t) {
2112 if (std::fabs(support.models[t].thresholdKG - thresholds[t]) > 1e-9) {
2113 std::cerr << "error: " << options.supportFile << " was built for a different threshold ("
2114 << support.models[t].thresholdKG * 1000. << " G against " << thresholds[t] * 1000. << " G)\n";
2115 return 1;
2116 }
2117 }
2118 if (!support.parameterisation.empty() && support.parameterisation != field->getParameterName()) {
2119 std::cerr << "error: " << options.supportFile << " was built for parameterisation "
2120 << support.parameterisation << ", not " << field->getParameterName() << '\n';
2121 return 1;
2122 }
2123 report(" support model : " + options.supportFile + " (cached)");
2124 } else {
2125 support = buildSupport(field, thresholds, -3000., 3000.);
2126 if (!options.supportFile.empty()) {
2127 std::ofstream out(options.supportFile);
2128 out << supportToJson(support, fieldSource).dump(1, '\t') << '\n';
2129 report(" support model : built and written to " + options.supportFile);
2130 } else {
2131 report(" support model : built for this run");
2132 }
2133 }
2134 support.marginStrict = options.margin;
2135
2136 std::string bandCounts;
2137 for (const auto& model : support.models) {
2138 bandCounts += form("%s%.1f G -> %zu bands", bandCounts.empty() ? "" : ", ", model.thresholdKG * 1000.,
2139 model.bands.size());
2140 }
2141 report(" " + bandCounts);
2142 report(form(" margins: strict %.2f cm, tight %.2f cm, edge uncertainty %.2f cm",
2143 support.marginStrict, support.marginTight, support.edgeUncertainty));
2144 report("");
2145
2146 // The model is only worth anything if it really is an outer bound, and only the
2147 // field itself can say so.
2148 report("outer-bound check");
2149 if (!violationScan(field, support, report)) {
2150 report("");
2151 report("The support model is not an outer bound on this field, so no placement can be called");
2152 report("field-free from it. Refusing to classify.");
2153 report.write(options.outputPrefix + "-report.txt");
2154 return 1;
2155 }
2156 report("");
2157
2158 // --- the geometry -----------------------------------------------------------
2159 TGeoManager::Import(options.geometryFile.c_str());
2160 if (gGeoManager == nullptr) {
2161 std::cerr << "error: no TGeoManager in " << options.geometryFile << '\n';
2162 return 1;
2163 }
2164 report(form(" volumes : %d, media %d", gGeoManager->GetListOfVolumes()->GetEntries(),
2165 gGeoManager->GetListOfMedia()->GetEntries()));
2166 report("");
2167 reportReachability(options.reachSamples, options.reachJobs, navigator, report);
2168
2169 Doctor doctor(field, support);
2170 doctor.walk(gGeoManager->GetTopNode());
2171 report(form(" placements : %zu classified, %zu detector subtrees pruned", doctor.rows().size(),
2172 doctor.nPruned()));
2173 report("");
2174
2175 progress("classify: sampling the field inside every placement that reaches the support");
2176 doctor.classifyAll();
2177 doctor.findFindings();
2178
2179 std::map<std::string, int> verdicts;
2180 for (const auto& row : doctor.rows()) {
2181 ++verdicts[row.verdict];
2182 }
2183 report("verdicts");
2184 for (const auto& verdict : verdicts) {
2185 report(form(" %-16s %7d", verdict.first.c_str(), verdict.second));
2186 }
2187 report("");
2188
2189 // --- the reverse audit ------------------------------------------------------
2190 const double threshold = support.models.front().thresholdKG;
2191 std::map<std::string, std::pair<int, double>> reverseByVolume;
2192 for (const auto* row : doctor.reverseAudit()) {
2193 auto& entry = reverseByVolume[row->lv + " [" + row->medium + "]"];
2194 ++entry.first;
2195 entry.second = std::max(entry.second, std::max(row->maxB, row->wholeMaxB));
2196 }
2197 int inRealField = 0;
2198 for (const auto& entry : reverseByVolume) {
2199 inRealField += entry.second.second > threshold ? 1 : 0;
2200 }
2201 report(form("reverse audit: %zu placements carry a field-free medium yet reach into the field support",
2202 doctor.reverseAudit().size()));
2203 report(form(" %zu logical volumes, %d of them with real field in their own material",
2204 reverseByVolume.size(), inRealField));
2205 report(form(" %-46s %11s %16s", "volume [medium]", "placements", "max |B| [kG]"));
2206 for (const auto& entry : reverseByVolume) {
2207 report(form(" %-46s %11d %16.4f%s", entry.first.c_str(), entry.second.first, entry.second.second,
2208 entry.second.second > threshold ? " <-- straight-line transport in real field" : ""));
2209 }
2210 report("");
2211
2212 // --- the forward findings ---------------------------------------------------
2213 report(form("shared volumes: %zu logical volumes are placed both out of and into the field",
2214 doctor.sharedVolumes().size()));
2215 report(form(" %-28s %8s %8s %s", "logical volume", "out", "in", "status"));
2216 for (size_t i = 0; i < doctor.sharedVolumes().size() && i < 20; ++i) {
2217 const auto& shared = doctor.sharedVolumes()[i];
2218 bool refused = false;
2219 bool approximate = false;
2220 for (const auto* row : shared.out) {
2221 refused = refused || row->sensitive || doctor.hasSensitive(row->node->GetVolume());
2222 approximate = approximate || row->approximateExtent;
2223 }
2224 report(form(" %-28s %8zu %8zu %s%s", shared.lv.c_str(), shared.out.size(), shared.in.size(),
2225 refused ? "refused by default (sensitive)" : "proposed",
2226 approximate ? " [extent approximate]" : ""));
2227 }
2228 if (doctor.sharedVolumes().size() > 20) {
2229 report(form(" ... and %zu more, all of them in the proposals file", doctor.sharedVolumes().size() - 20));
2230 }
2231 report("");
2232
2233 report(form("heterogeneous mothers: %zu whose own material straddles the predicate",
2234 doctor.straddlingMothers().size()));
2235 for (size_t i = 0; i < doctor.straddlingMothers().size() && i < 10; ++i) {
2236 const auto* row = doctor.straddlingMothers()[i];
2237 report(form(" %-40s |B| in own material %.4g .. %.4f kG, %d daughters", row->lv.c_str(), row->wholeMinB,
2238 row->wholeMaxB, row->nDaughters));
2239 }
2240 report("");
2241
2242 report(form("missing containers: %zu daughter clusters lie wholly on the field-free side",
2243 doctor.containers().size()));
2244 for (const auto& container : doctor.containers()) {
2245 report(form(" in %-14s z %9.2f .. %9.2f rmax %7.2f %3d daughters %s%s", container.mother.c_str(),
2246 container.zlo, container.zhi, container.rmax, container.nDaughters,
2247 container.clearedByStrictMargin ? "clear by the strict margin" : "clear by the tight margin",
2248 container.sensitive ? " [refused: sensitive]" : ""));
2249 }
2250 report("");
2251
2252 // --- the anchor self-check --------------------------------------------------
2253 bool anchorsPassed = true;
2254 if (!options.anchorFile.empty()) {
2255 report("anchors");
2256 anchorsPassed = verifyAnchors(options.anchorFile, doctor, report);
2257 report(anchorsPassed ? " all anchors reproduced" : " ANCHORS FAILED");
2258 report("");
2259 }
2260
2261 // --- outputs ----------------------------------------------------------------
2262 const std::string proposalsPath = options.outputPrefix + "-proposals.json";
2263 const std::string tablePath = options.outputPrefix + "-placements.csv";
2264 const std::string reportPath = options.outputPrefix + "-report.txt";
2265 std::ofstream proposals(proposalsPath);
2266 proposals << proposalsToJson(doctor, support, options.geometryFile, fieldSource).dump(1, '\t') << '\n';
2267 writePlacementCsv(doctor.rows(), tablePath);
2268 report("wrote " + proposalsPath + ", " + tablePath + " and " + reportPath);
2269 report.write(reportPath);
2270
2271 return anchorsPassed ? 0 : 2;
2272}
header::DataOrigin origin
header::DataDescription description
Definition of the GeometryManager class.
std::unique_ptr< expressions::Node > node
int32_t i
bool done
GPUChain * chain
Definition of the MagF class.
std::vector< SidecarEdge > edges
uint32_t j
Definition RawData.h:0
static bool vecGeomLocate(double x, double y, double z, std::vector< TGeoNode * > &chain)
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
GLuint64EXT * result
Definition glcorearb.h:5662
GLuint buffer
Definition glcorearb.h:655
GLuint entry
Definition glcorearb.h:5735
GLsizeiptr size
Definition glcorearb.h:659
GLuint index
Definition glcorearb.h:781
GLuint const GLchar * name
Definition glcorearb.h:781
GLsizei samples
Definition glcorearb.h:1309
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
nlohmann::json json
std::map< std::string, ID > expected
VectorOfTObjectPtrs other
#define main
std::vector< int > row
std::vector< ReadoutWindowData > rows