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
configStandalone Light OpenGL display display_light Apply smoothing to points Enable Z buffer Highlight links Highlight seeds Highlight sector tracks Highlight final tracks Show individual Show related Highlight grid Show only individual collision Color clusters belonging to track objects Set line Enable drawing TPC data Enable drawing TOF data Enable drawing magnetic field Set field line step count Invert colors configStandalone Heavy OpenGL display display_heavy Show tracks with ITS contribution Show tracks with TOF contribution Enable propagation of loopers Show clusters at nominal x of pad row for early transformed data Split CE tracks when they cross the central Mark clusters Mark adjacent clusters Hide rejected tracks Apply filter on tracks to be displayed Separate increase X h configStandalone Camera window renderer OpenGL display display_camera Orient the camera such that the y axis is always upwards Full Screen MultiSample Anti Downsampling Anti Enable Vertical Sync Target frame or if the backend cannot limit to the refresh Use OpenGL indirect draws to reduce number of draw calls Display h configStandalone Vulkan display display_vulkan Have uniform buffers in host accessible device memory h configStandalone Internal display display_internal Number of updates the Qt GUI may send ahead of the last one the display has before it waits for the display Opacity of the console background Number of console lines the history is shortened to this when it reaches twice the size Time in seconds an info message fades out after setInfoTime h OpenGL display display Font size Do not use Freetype for font Renderer for event Frontend to use for event ROOT macros used as track filter Console script file
Timeframe settings Average distance in cm of events merged into Randomize distance around average distance of merged events Simulate correct bunch interactions instead of placing only the average number of events A value[n] sets TFlen for[n] collisions in Number of bunches per trainsort Number of bunch trains Instantaneous interaction rate Do not simulate border Total number of collisions to be placed in the interior of all time Do not select random but walk over array of events in stride steps h Event generator settings h configStandalone Use CPU for GPU Force usage of the specified GPU device no CPU fallback Continue processing after an error First event to Number of events to Number of iterations to Number of iterations to Number of initial iterations excluded from const char e
Timeframe settings Average distance in cm of events merged into Randomize distance around average distance of merged events Simulate correct bunch interactions instead of placing only the average number of events A value[n] sets TFlen for[n] collisions in Number of bunches per trainsort Number of bunch trains Instantaneous interaction rate Do not simulate border Total number of collisions to be placed in the interior of all time Do not select random but walk over array of events in stride steps h Event generator settings h configStandalone Use CPU for GPU Force usage of the specified GPU device no CPU fallback Continue processing after an error First event to Number of events to Number of iterations to Number of iterations to Number of initial iterations excluded from const char Directory with events to Events directory is and not inside events d
configStandalone Reconstruction rec_tpc multiplier for the combined cluster track error during track following area in cm for the search of for z only used if decrease removal by when above this local occupancy rowx Shift of TPC revert cluster flag edge bit distance to edge exceeds this ZOffset for secondary multiple of radius max P of electron K BB bands crossing max P of electron p BB bands crossing max P of electron d BB bands crossing max P of electron t BB bands crossing min accepted dEdxMaxTPC of the track Additive extra cluster error for Y2 if edge flag set Additive extra cluster error for Y2 if single set Additive extra cluster error for Y2 if shared
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
GLuint64EXT * result
Definition glcorearb.h:5662
GLuint entry
Definition glcorearb.h:5735
GLsizeiptr size
Definition glcorearb.h:659
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
GLint level
Definition glcorearb.h:275
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