51#include <TGeoManager.h>
52#include <TGeoMatrix.h>
53#include <TGeoMedium.h>
54#include <TGeoNavigator.h>
60#include <TGeoVolume.h>
64#include <boost/program_options.hpp>
65#include <nlohmann/json.hpp>
83using json = nlohmann::json;
93template <
typename... Args>
94std::string form(
const char*
fmt, Args... args)
98 return std::string(
buffer);
105 void operator()(
const std::string& line)
107 std::cout << line <<
'\n';
108 mLines.push_back(line);
110 void write(
const std::string&
path)
const
112 std::ofstream out(
path);
113 for (
const auto& line : mLines) {
119 std::vector<std::string> mLines;
123void progress(
const std::string& line) { std::cerr << line << std::endl; }
144constexpr const char* kFieldObjectKey =
"MagneticField";
145constexpr const char* kFieldProbeKey =
"ReferenceProbes";
150 const int n = probes.GetNrows() / 6;
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.};
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);
163 return same ? 0 : (flipped ? 1 : -1);
168 TFile* file = TFile::Open(
path.c_str());
169 if (file ==
nullptr || file->IsZombie()) {
170 progress(
"error: cannot open field file " +
path);
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()));
179 if (probes ==
nullptr) {
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()));
196 int comparison = compareToProbes(field,
reference);
197 if (comparison == 1) {
200 comparison = compareToProbes(field,
reference);
201 if (comparison == 0) {
202 progress(
"field: polarity flip from the non-idempotent CreateField() detected and repaired");
205 if (comparison != 0) {
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",
212 progress(form(
"field: %s verified against %d reference probe vectors",
path.c_str(),
reference.GetNrows() / 6));
218 double point[3] = {
x,
y,
z};
219 double b[3] = {0., 0., 0.};
221 return std::sqrt(
b[0] *
b[0] +
b[1] *
b[1] +
b[2] *
b[2]);
226 return fieldMag(field,
r * std::cos(phi),
r * std::sin(phi),
z);
259constexpr int kMinPhiSamples = 24;
260constexpr double kPhiArcStep = 3.0;
261constexpr double kBisectionTol = 0.01;
262constexpr double kZStepCoarse = 1.0;
263constexpr double kZStepRefine = 0.1;
264constexpr double kScanRMax = 2100.;
265constexpr double kScanRMaxFine = 900.;
266constexpr double kTightMargin = 0.05;
267constexpr long kViolationScanPoints = 400000;
275 std::vector<Interval> iv;
279 double thresholdKG = 0.;
280 std::vector<Band> bands;
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;
295 double separation(
int t,
double vzmin,
double vzmax,
double vrmin,
double vrmax)
const
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));
310 return (best > 1e29) ? 1e30 : best;
313 bool supportAt(
int t,
double z,
double r)
const {
return separation(t,
z,
z,
r,
r) <= 0.; }
319 double penetration(
int t,
double vzmin,
double vzmax,
double vrmin,
double vrmax)
const
322 for (
const auto& band : models[t].bands) {
323 if (band.zlo > vzmax || band.zhi < vzmin) {
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) {
332 worst = std::max(worst, std::min(hi - vrmin, vrmax - lo));
345 bool coveredBySupport(
int t,
double vzmin,
double vzmax,
double vrmin,
double vrmax)
const
347 std::vector<std::pair<double, double>> covering;
348 for (
const auto& band : models[t].bands) {
349 if (band.zhi < vzmin || band.zlo > vzmax) {
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});
359 std::sort(covering.begin(), covering.end());
360 double frontier = vzmin;
361 for (
const auto&
segment : covering) {
362 if (
segment.first > frontier + 1e-9) {
365 frontier = std::max(frontier,
segment.second);
366 if (frontier >= vzmax) {
370 return frontier >= vzmax;
374 bool inDomain(
double vzmin,
double vzmax,
double vrmax)
const
376 return vzmin >= zmin && vzmax <= zmax && vrmax <= rmax;
380int phiSamplesAt(
double r)
385 return std::max(kMinPhiSamples, (
int)std::ceil(2 * M_PI *
r / kPhiArcStep));
391 return fieldMag(field, 0., 0.,
z);
393 const int n = phiSamplesAt(
r);
395 for (
int i = 0;
i <
n; ++
i) {
396 worst = std::max(worst, fieldMagCyl(field,
r, 2 * M_PI *
i /
n,
z));
401std::vector<double> radialGrid()
403 std::vector<double> grid;
404 for (
double r = 0.;
r < 20.;
r += 0.1) {
407 for (
double r = 20.;
r < 100.;
r += 1.0) {
410 for (
double r = 100.;
r < 800.;
r += 2.0) {
413 for (
double r = 800.;
r <= kScanRMaxFine;
r += 10.0) {
417 for (
double r = kScanRMaxFine + 25.;
r <= kScanRMax;
r += 25.0) {
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) {
437 return 0.5 * (rOut + rIn);
441 double z = 0., zlo = 0., zhi = 0.;
442 std::vector<std::vector<Interval>> iv;
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);
455 for (
size_t t = 0; t < thresholds.size(); ++t) {
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];
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);
470 current.hi = grid.back();
471 slice.iv[t].push_back(current);
477void mergeIntervals(std::vector<Interval>& into,
const std::vector<Interval>& from)
479 into.insert(into.end(), from.begin(), from.end());
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);
489 merged.push_back(into[
i]);
495bool sameStructure(
const std::vector<Interval>&
a,
const std::vector<Interval>&
b)
497 if (
a.size() !=
b.size()) {
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) {
508Support buildSupport(
o2::field::MagneticField* field,
const std::vector<double>& thresholds,
double zmin,
double zmax)
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.)));
515 std::vector<Slice> slices;
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]);
525 for (
double zz = previous.z + kZStepRefine; zz <
z - 1e-9; zz += kZStepRefine) {
526 slices.push_back(sliceAt(field, zz, thresholds));
529 slices.push_back(slice);
532 if (std::fmod(
z - zmin, 500.) < kZStepCoarse / 2) {
533 progress(form(
"support: ... z = %.0f",
z));
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);
552 support.models.resize(thresholds.size());
553 for (
size_t t = 0; t < thresholds.size(); ++t) {
554 support.models[t].thresholdKG = thresholds[t];
556 while (
i < slices.size()) {
558 while (
j + 1 < slices.size() && sameStructure(slices[
j].iv[t], slices[
j + 1].iv[t])) {
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]);
567 if (!band.iv.empty()) {
568 support.models[t].bands.push_back(band);
575 support.rmax = kScanRMax;
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) {
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)) {
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]));
625json supportToJson(
const Support& support,
const std::string& fieldSource)
627 const std::time_t now = std::time(
nullptr);
629 std::strftime(stamp,
sizeof(stamp),
"%Y-%m-%dT%H:%M:%S", std::gmtime(&now));
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";
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) {
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) {
660 b[
"iv"] = json::array();
661 for (
const auto& iv : band.iv) {
662 b[
"iv"].push_back(json::array({iv.lo, iv.hi}));
664 m[
"bands"].push_back(
b);
666 out[
"models"].push_back(
m);
673bool supportFromJson(
const json& in, Support& support)
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")) {
685 model.thresholdKG =
m.at(
"threshold_kG").get<
double>();
686 for (
const auto&
b :
m.at(
"bands")) {
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>()});
693 model.bands.push_back(band);
695 support.models.push_back(model);
697 }
catch (
const std::exception& e) {
698 progress(std::string(
"error: cannot read the support model: ") + e.what());
701 if (support.models.empty() || support.models.front().bands.empty()) {
703 "error: the support model is empty -- refusing to continue, since an empty model "
704 "would declare the whole geometry field-free");
736 std::string medium, mother, worstPath;
738 double fraction = 1.;
740 double ownFraction = 1.;
741 double vgFraction = 1.;
742 double vgOwnFraction = 1.;
746constexpr int kReachRejectionTries = 400;
752enum class Navigator { TGeo,
759bool insideAnyDaughter(TGeoVolume* volume,
const double* local);
766 TGeoNode*
node =
nullptr;
769 std::vector<TGeoNode*>
chain;
770 std::vector<TGeoNode*> flatChain;
774 bool sampled =
false;
775 long drawn = 0, reached = 0, ownDrawn = 0, ownReached = 0;
776 long vgReached = 0, vgOwnReached = 0, disagreed = 0;
785 void walk(TGeoNode*
node) { walk(
node, TGeoHMatrix(),
"", {}, {}); }
786 std::vector<ReachTask>& tasks() {
return mTasks; }
787 long nodesVisited()
const {
return mVisited; }
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;
797void ReachCollector::walk(TGeoNode*
node,
const TGeoHMatrix& parent,
const std::string&
path,
798 std::vector<TGeoNode*>
chain, std::vector<TGeoNode*> flatChain)
800 if (!mSeen.insert(
node).second) {
803 TGeoHMatrix here = parent;
804 here.Multiply(
node->GetMatrix());
805 const std::string myPath =
path +
"/" +
node->GetName();
810 if (!
node->GetVolume()->IsAssembly()) {
811 flatChain.push_back(
node);
817 task.flatChain = flatChain;
818 mTasks.push_back(std::move(task));
820 for (
int i = 0;
i <
node->GetNdaughters(); ++
i) {
821 walk(
node->GetDaughter(
i), here, myPath,
chain, flatChain);
829bool samplePoint(TGeoShape* shape, TRandom3& random,
double* local)
832 if (
box ==
nullptr) {
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)) {
852unsigned int seedFor(
size_t index)
854 unsigned long long x = 20260901ull + 0x9E3779B97F4A7C15ull * (
index + 1);
856 x *= 0xBF58476D1CE4E5B9ull;
858 return (
unsigned int)(
x >> 33) | 1u;
865bool tgeoPassesThrough(TGeoNavigator* nav,
const std::vector<TGeoNode*>&
chain,
bool& exact)
868 const int level = nav->GetLevel();
872 for (
int d = 0; d <=
depth; ++d) {
887bool sameFlattenedNode(TGeoNode* located, TGeoNode* wanted)
889 if (located == wanted) {
892 if (located->GetVolume() != wanted->GetVolume()) {
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;
905bool vecGeomPassesThrough(
const std::vector<TGeoNode*>& located,
const std::vector<TGeoNode*>& flatChain,
bool& exact)
907 const int depth = (
int)flatChain.size() - 1;
908 if ((
int)located.size() - 1 <
depth) {
911 for (
int d = 0; d <=
depth; ++d) {
912 if (!sameFlattenedNode(located[d], flatChain[d])) {
916 exact = ((
int)located.size() - 1 ==
depth);
920void sampleTask(
const ReachTask& task,
size_t index,
int samples, Navigator backend, TGeoNavigator* nav,
921 std::vector<TGeoNode*>& located, ReachResult& out)
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));
929 double local[3], global[3];
930 if (!samplePoint(volume->GetShape(), random, local)) {
936 const bool own = !hasDaughters || !insideAnyDaughter(volume, local);
940 task.matrix.LocalToMaster(local, global);
942 bool tgeoThrough =
false, tgeoExact =
false;
951 if (nav->FindNode(global[0], global[1], global[2]) !=
nullptr) {
952 tgeoThrough = tgeoPassesThrough(nav, task.chain, tgeoExact);
956 if (own && tgeoExact) {
962 bool vgThrough =
false, vgExact =
false;
964 vgThrough = vecGeomPassesThrough(located, task.flatChain, vgExact);
968 if (own && vgExact) {
972 if (backend == Navigator::Both && vgThrough != tgeoThrough) {
977 out.sampled = out.drawn > 0;
981long reportReachability(
int samples,
int jobs, Navigator backend, Report&
report)
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();
992 report(
" VecGeom backend requested but this build of O2 has none; falling back to TGeo");
993 backend = Navigator::TGeo;
996 int threads = jobs > 0 ? jobs : (
int)std::thread::hardware_concurrency();
997 threads = std::max(1, std::min<int>(threads, (
int)tasks.size()));
1003 gGeoManager->SetMaxThreads(threads);
1005 progress(form(
"reachability: %zu placements, %d point%s each, %d thread%s", tasks.size(),
samples,
1006 samples == 1 ?
"" :
"s", threads, threads == 1 ?
"" :
"s"));
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;
1015 for (
size_t i = next++;
i < tasks.size();
i = next++) {
1016 sampleTask(tasks[
i],
i,
samples, backend, nav, located, results[
i]);
1020 std::vector<std::thread> pool;
1021 pool.reserve(threads);
1022 for (
int i = 0;
i < threads; ++
i) {
1023 pool.emplace_back(worker);
1025 for (
auto& thread : pool) {
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];
1041 TGeoNode*
node = tasks[
i].node;
1042 auto* medium =
node->GetVolume()->GetMedium();
1044 entry.medium = medium !=
nullptr ? medium->GetName() :
"(none)";
1045 entry.mother =
node->GetMotherVolume() !=
nullptr ?
node->GetMotherVolume()->GetName() :
"-";
1046 entry.worstPath = tasks[
i].path;
1050 const bool primaryIsVecGeom = backend == Navigator::VecGeom;
1053 ? double(primaryIsVecGeom ?
result.vgOwnReached :
result.ownReached) /
result.ownDrawn
1057 if (
result.disagreed > 0) {
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);
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(),
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);
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) {
1085 entry.worstPath.c_str()));
1088 for (
size_t i = 0;
i < partial.size() &&
i < 20; ++
i) {
1089 const auto&
entry = partial[
i];
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"));
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()));
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)));
1104 if (backend == Navigator::Both) {
1106 if (disagreeing == 0) {
1107 report(
" TGeo and VecGeom agree on every point sampled.");
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) {
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);
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;
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(),
1137 entry.worstPath.c_str()));
1139 if (conflicts.size() > 20) {
1140 report(form(
" ... and %zu more", conflicts.size() - 20));
1145 return (
long)dead.size();
1152constexpr int kMaxDepth = 14;
1153constexpr size_t kMaxRows = 400000;
1154constexpr double kSampleDr = 0.05;
1155constexpr double kSampleArc = 0.5;
1156constexpr double kSampleDzMax = 2.0;
1157constexpr long kMaxSamplesPerRow = 4000000;
1160 std::string
path, lv, medium, mother, shape;
1161 std::string effectiveMother;
1162 std::string verdict =
"UNCLASSIFIED";
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.;
1170 bool wholeVolumeSampled =
false;
1171 double wholeMaxB = -1., wholeMinB = -1.;
1172 TGeoNode*
node =
nullptr;
1176bool isOutFamily(
const std::string& verdict)
1178 return verdict ==
"OUT" || verdict ==
"OUT_TIGHT" || verdict ==
"OUT_BOUNDARY";
1185bool isInFamily(
const std::string& verdict)
1187 return verdict ==
"IN" || verdict ==
"IN_COVERED" || verdict ==
"UNKNOWN" || verdict ==
"OUTSIDE_DOMAIN";
1198bool shapeRadii(TGeoShape* shape,
double& rmin,
double& rmax)
1200 if (
auto* pgon =
dynamic_cast<TGeoPgon*
>(shape)) {
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));
1207 const double edges = pgon->GetNedges() > 2 ? pgon->GetNedges() : 3;
1208 rmax /= std::cos(M_PI /
edges);
1211 if (
auto* pcon =
dynamic_cast<TGeoPcon*
>(shape)) {
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));
1220 if (
auto* cone =
dynamic_cast<TGeoCone*
>(shape)) {
1221 rmin = std::min(cone->GetRmin1(), cone->GetRmin2());
1222 rmax = std::max(cone->GetRmax1(), cone->GetRmax2());
1225 if (
auto* eltu =
dynamic_cast<TGeoEltu*
>(shape)) {
1227 rmax = std::max(eltu->GetA(), eltu->GetB());
1230 if (
auto* tube =
dynamic_cast<TGeoTube*
>(shape)) {
1231 rmin = tube->GetRmin();
1232 rmax = tube->GetRmax();
1238bool zPreserving(
const TGeoHMatrix&
m)
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;
1245void extentOf(TGeoNode*
node,
const TGeoHMatrix& matrix, Row&
row)
1247 TGeoShape* shape =
node->GetVolume()->GetShape();
1249 if (
box ==
nullptr) {
1252 row.zmin =
row.zmax = 0.;
1255 row.approximateExtent =
true;
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)};
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]));
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;
1283 row.rmin = (offAxis <= std::hypot(dx, dy)) ? 0. : std::max(0., offAxis - std::hypot(dx, dy));
1284 row.approximateExtent =
true;
1292struct ContainerProposal {
1293 std::string mother, motherPath;
1294 double zlo, zhi, rmax;
1296 bool clearedByStrictMargin;
1300struct SharedVolume {
1302 std::vector<const Row*> out, in;
1310 mThreshold = support.models.front().thresholdKG;
1313 void walk(TGeoNode*
node) { walk(
node,
nullptr, TGeoHMatrix(), 0,
"",
""); }
1315 void findFindings();
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);
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;
1334 const Support& mSupport;
1336 std::vector<Row> mRows;
1337 std::map<TGeoVolume*, int> mSensitiveCache;
1340 std::vector<const Row*> mReverse;
1341 std::vector<SharedVolume> mShared;
1342 std::vector<const Row*> mStraddling;
1343 std::vector<ContainerProposal> mContainers;
1346bool Doctor::hasSensitive(TGeoVolume* volume)
1348 auto cached = mSensitiveCache.find(volume);
1349 if (cached != mSensitiveCache.end() && cached->second >= 0) {
1350 return cached->second == 1;
1352 mSensitiveCache[volume] = 0;
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());
1358 mSensitiveCache[volume] = found ? 1 : 0;
1368bool isStructural(TGeoNode*
node, TGeoVolume* mother)
1370 if (mother ==
nullptr) {
1373 TGeoVolume* volume =
node->GetVolume();
1374 if (volume->IsAssembly()) {
1377 auto* mine = volume->GetMedium();
1378 auto* theirs = mother->GetMedium();
1379 return mine !=
nullptr && theirs !=
nullptr && std::strcmp(mine->GetName(), theirs->GetName()) == 0;
1382void Doctor::walk(TGeoNode*
node, TGeoVolume* mother,
const TGeoHMatrix& parent,
int depth,
const std::string&
path,
1383 const std::string& effectiveMother)
1385 if (mRows.size() >= kMaxRows ||
depth > kMaxDepth) {
1388 TGeoHMatrix here = parent;
1389 here.Multiply(
node->GetMatrix());
1390 TGeoVolume* volume =
node->GetVolume();
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();
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.;
1410 mRows.push_back(
row);
1412 if (hasSensitive(volume) && !isStructural(
node, mother)) {
1416 for (
int i = 0;
i <
node->GetNdaughters(); ++
i) {
1417 walk(
node->GetDaughter(
i), volume, here,
depth + 1, myPath,
row.assembly ? effectiveMother : myPath);
1426bool insideAnyDaughter(TGeoVolume* volume,
const double* local)
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)) {
1435 if (daughter->GetVolume()->IsAssembly()) {
1436 if (insideAnyDaughter(daughter->GetVolume(), inDaughter)) {
1446bool Doctor::ownMaterialAt(
const Row&
row,
const double* global)
const
1449 row.matrix.MasterToLocal(global, local);
1450 if (!
row.node->GetVolume()->GetShape()->Contains(local)) {
1453 return !insideAnyDaughter(
row.node->GetVolume(), local);
1456double thinnestDaughter(TGeoVolume* volume)
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())));
1471void Doctor::disproofScan(Row&
row)
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);
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);
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};
1496 if (!ownMaterialAt(
row, global)) {
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);
1509 row.resolved =
false;
1516void Doctor::wholeVolumeScan(Row&
row)
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};
1529 if (!ownMaterialAt(
row, global)) {
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);
1540void Doctor::classify(Row&
row)
1543 row.verdict =
"ASSEMBLY";
1546 if (!mSupport.inDomain(
row.zmin,
row.zmax,
row.rmax)) {
1547 row.verdict =
"OUTSIDE_DOMAIN";
1550 row.separation = mSupport.separation(0,
row.zmin,
row.zmax,
row.rmin,
row.rmax);
1551 if (
row.separation >= mSupport.marginStrict) {
1552 row.verdict =
"OUT";
1555 if (
row.separation >= mSupport.marginTight) {
1556 row.verdict =
"OUT_TIGHT";
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";
1565 if (
row.maxB > mThreshold) {
1567 }
else if (
row.separation > 0. ||
row.penetration <= 2 * mSupport.edgeUncertainty) {
1571 row.verdict =
"OUT_BOUNDARY";
1573 row.verdict =
"UNKNOWN";
1577void Doctor::classifyAll()
1580 for (
auto&
row : mRows) {
1582 if (++
done % 50000 == 0) {
1583 progress(form(
"classify: %zu / %zu placements",
done, mRows.size()));
1586 for (
auto&
row : mRows) {
1589 if (
row.nDaughters > 0 && !
row.assembly &&
1590 (
row.verdict ==
"IN" ||
row.verdict ==
"UNKNOWN" ||
row.verdict ==
"OUT_BOUNDARY")) {
1591 wholeVolumeScan(
row);
1596void Doctor::findFindings()
1603 for (
auto&
row : mRows) {
1604 if (
row.assembly ||
row.ifield != 0 ||
row.medium ==
"dummy" ||
row.medium ==
"(none)") {
1607 if (isInFamily(
row.verdict)) {
1608 if (!
row.wholeVolumeSampled) {
1609 wholeVolumeScan(
row);
1611 mReverse.push_back(&
row);
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);
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);
1632 if (!shared.out.empty() && !shared.in.empty()) {
1633 mShared.push_back(shared);
1636 std::sort(mShared.begin(), mShared.end(),
1637 [](
const SharedVolume&
a,
const SharedVolume&
b) { return a.out.size() > b.out.size(); });
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);
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);
1662 std::sort(kids.begin(), kids.end(), [](
const Row*
a,
const Row*
b) { return a->zmin < b->zmin; });
1665 while (
i < kids.size()) {
1666 if (!isOutFamily(kids[
i]->verdict)) {
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)) {
1674 zlo = std::min(zlo, kids[
j]->zmin);
1675 zhi = std::max(zhi, kids[
j]->zmax);
1676 rmax = std::max(rmax, kids[
j]->rmax);
1678 const int n = (
int)(
j -
i + 1);
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) {
1691 const Row*
other = kids[k];
1692 if (
other->zmax <= zlo + 0.01 ||
other->zmin >= zhi - 0.01 ||
other->rmin >= rmax - 0.01) {
1695 intruders.push_back(
other);
1697 for (
const Row*
other : intruders) {
1698 if (
other->zmin <= clampedLo + 1e-9 &&
other->zmax > clampedLo) {
1699 clampedLo =
other->zmax;
1701 if (
other->zmax >= clampedHi - 1e-9 &&
other->zmin < clampedHi) {
1702 clampedHi =
other->zmin;
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);
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());
1726 mContainers.push_back(proposal);
1737void writePlacementCsv(
const std::vector<Row>&
rows,
const std::string&
path)
1739 std::FILE* out = std::fopen(
path.c_str(),
"w");
1740 if (out ==
nullptr) {
1741 progress(
"error: cannot write " +
path);
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,
1756json placementJson(
const Row&
row)
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)}};
1767json proposalsToJson(Doctor& doctor,
const Support& support,
const std::string& geometryFile,
1768 const std::string& fieldSource)
1770 const std::time_t now = std::time(
nullptr);
1772 std::strftime(stamp,
sizeof(stamp),
"%Y-%m-%dT%H:%M:%S", std::gmtime(&now));
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();
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());
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));
1801 entry[
"in_placements"] = json::array();
1802 for (
const auto*
row : shared.in) {
1803 entry[
"in_placements"].push_back(placementJson(*
row));
1805 out[
"proposals"].push_back(
entry);
1808 for (
const auto*
row : doctor.reverseAudit()) {
1809 const double maxB = std::max(
row->maxB,
row->wholeMaxB);
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";
1821 entry[
"min_B_kG"] =
row->wholeMinB;
1822 entry[
"max_B_kG"] = maxB;
1824 out[
"proposals"].push_back(
entry);
1827 for (
const auto& container : doctor.containers()) {
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);
1841 for (
const auto*
row : doctor.straddlingMothers()) {
1843 entry[
"signature"] =
"heterogeneous-mother";
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";
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);
1872bool verifyAnchors(
const std::string&
path, Doctor& doctor, Report&
report)
1874 std::ifstream in(
path);
1876 report(
" cannot open the anchor file " +
path);
1882 }
catch (
const std::exception& e) {
1883 report(std::string(
" cannot parse the anchor file: ") + e.what());
1887 std::set<std::string> flaggedByReverseAudit;
1888 for (
const auto*
row : doctor.reverseAudit()) {
1889 flaggedByReverseAudit.insert(
row->lv);
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>();
1900 double worstSeparation = 1e30;
1901 double bestField = -1.;
1902 std::string reported;
1903 for (
const auto&
row : doctor.rows()) {
1904 if (
row.lv != volume) {
1910 ok = isOutFamily(
row.verdict) ||
row.verdict ==
"ASSEMBLY";
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") {
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;
1931 if (
expected ==
"REVERSE_AUDIT_FLAGGED") {
1932 failures = flaggedByReverseAudit.count(volume) > 0 ? 0 : 1;
1933 reported = failures == 0 ?
"flagged" :
"not flagged";
1935 if (placements == 0) {
1937 reported =
"not placed";
1939 if (anchor.contains(
"placements") && placements != anchor.at(
"placements").get<
int>()) {
1942 if (anchor.contains(
"min_max_B_kG") && bestField < anchor.at(
"min_max_B_kG").get<
double>()) {
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"));
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;
1966 std::string navigator =
"tgeo";
1967 bool reachabilityOnly =
false;
1968 std::string outputPrefix =
"geometry-doctor";
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");
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");
2016 }
catch (
const bpo::error& e) {
2017 std::cerr <<
"error: " << e.what() <<
"\n\n"
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";
2032 const bool haveFieldFile =
arguments.count(
"field-file") != 0u;
2033 const bool haveFieldCurrent =
arguments.count(
"field-current") != 0u;
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';
2044 report(
"ALICE simulation geometry doctor -- reachability audit");
2046 report(
" geometry : " + options.geometryFile);
2047 report(form(
" volumes : %d, media %d", gGeoManager->GetListOfVolumes()->GetEntries(),
2048 gGeoManager->GetListOfMedia()->GetEntries()));
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;
2057 if (haveFieldFile == haveFieldCurrent) {
2058 std::cerr <<
"error: give exactly one of --field-file and --field-current\n";
2061 if (options.thresholdsGauss.empty()) {
2062 options.thresholdsGauss = {1., 10.};
2064 std::sort(options.thresholdsGauss.begin(), options.thresholdsGauss.end());
2065 std::vector<double> thresholds;
2066 for (
double gauss : options.thresholdsGauss) {
2067 thresholds.push_back(gauss * 1e-3);
2070 const std::string fieldSource =
2071 haveFieldFile ? options.fieldFile : form(
"createNominalField(%d)", options.fieldCurrent);
2074 if (field ==
nullptr) {
2075 std::cerr <<
"error: no usable magnetic field\n";
2080 report(
"ALICE simulation geometry doctor");
2082 report(
" geometry : " + options.geometryFile);
2087 bool supportFromCache =
false;
2088 if (!options.supportFile.empty()) {
2089 std::ifstream cache(options.supportFile);
2094 }
catch (
const std::exception& e) {
2095 std::cerr <<
"error: cannot parse " << options.supportFile <<
": " << e.what() <<
'\n';
2098 if (!supportFromJson(cached, support)) {
2101 supportFromCache =
true;
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";
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";
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';
2123 report(
" support model : " + options.supportFile +
" (cached)");
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);
2131 report(
" support model : built for this run");
2134 support.marginStrict = options.margin;
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());
2141 report(
" " + bandCounts);
2142 report(form(
" margins: strict %.2f cm, tight %.2f cm, edge uncertainty %.2f cm",
2143 support.marginStrict, support.marginTight, support.edgeUncertainty));
2148 report(
"outer-bound check");
2149 if (!violationScan(field, support,
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");
2159 TGeoManager::Import(options.geometryFile.c_str());
2160 if (gGeoManager ==
nullptr) {
2161 std::cerr <<
"error: no TGeoManager in " << options.geometryFile <<
'\n';
2164 report(form(
" volumes : %d, media %d", gGeoManager->GetListOfVolumes()->GetEntries(),
2165 gGeoManager->GetListOfMedia()->GetEntries()));
2167 reportReachability(options.reachSamples, options.reachJobs, navigator,
report);
2169 Doctor doctor(field, support);
2170 doctor.walk(gGeoManager->GetTopNode());
2171 report(form(
" placements : %zu classified, %zu detector subtrees pruned", doctor.rows().size(),
2175 progress(
"classify: sampling the field inside every placement that reaches the support");
2176 doctor.classifyAll();
2177 doctor.findFindings();
2179 std::map<std::string, int> verdicts;
2180 for (
const auto&
row : doctor.rows()) {
2181 ++verdicts[
row.verdict];
2184 for (
const auto& verdict : verdicts) {
2185 report(form(
" %-16s %7d", verdict.first.c_str(), verdict.second));
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 +
"]"];
2195 entry.second = std::max(
entry.second, std::max(
row->maxB,
row->wholeMaxB));
2197 int inRealField = 0;
2198 for (
const auto&
entry : reverseByVolume) {
2199 inRealField +=
entry.second.second > threshold ? 1 : 0;
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) {
2208 entry.second.second > threshold ?
" <-- straight-line transport in real field" :
""));
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;
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]" :
""));
2228 if (doctor.sharedVolumes().size() > 20) {
2229 report(form(
" ... and %zu more, all of them in the proposals file", doctor.sharedVolumes().size() - 20));
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));
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]" :
""));
2253 bool anchorsPassed =
true;
2254 if (!options.anchorFile.empty()) {
2256 anchorsPassed = verifyAnchors(options.anchorFile, doctor,
report);
2257 report(anchorsPassed ?
" all anchors reproduced" :
" ANCHORS FAILED");
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);
2271 return anchorsPassed ? 0 : 2;
header::DataOrigin origin
header::DataDescription description
Definition of the GeometryManager class.
std::unique_ptr< expressions::Node > node
Definition of the MagF class.
std::vector< SidecarEdge > edges
static bool ensureVecGeomWorld()
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
GLuint const GLchar * name
GLboolean GLboolean GLboolean b
GLint GLint GLsizei GLsizei GLsizei depth
GLsizei const GLchar *const * path
GLboolean GLboolean GLboolean GLboolean a
GLsizei const GLint * box
GLdouble GLdouble GLdouble z
void report(gsl::span< o2::InteractionTimeRecord > irs, int threshold, bool verbose)
bpo::variables_map arguments
std::string to_string(gsl::span< T, Size > span)
std::map< std::string, ID > expected
VectorOfTObjectPtrs other
std::vector< ReadoutWindowData > rows