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 + 1
e-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 + 1
e-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 + 1
e-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 - 1
e-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();
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) {
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.) < 1
e-9 && std::fabs(
r[2]) < 1
e-9 && std::fabs(
r[5]) < 1
e-9 &&
1242 std::fabs(
r[6]) < 1
e-9 && std::fabs(
r[7]) < 1
e-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 + 1
e-9 && budget > 0;
z += dz) {
1490 for (
double r = r0;
r <= r1 + 1
e-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) {
1625 for (
const auto*
row :
entry.second) {
1626 if (isOutFamily(
row->verdict)) {
1628 }
else if (isInFamily(
row->verdict)) {
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 + 1
e-9 &&
other->zmax > clampedLo) {
1699 clampedLo =
other->zmax;
1701 if (
other->zmax >= clampedHi - 1
e-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;
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";
1796 entry[
"status"] = refused ?
"refused by default (sensitive path)" :
"proposed";
1797 entry[
"out_placements"] = json::array();
1799 entry[
"out_placements"].push_back(placementJson(*
row));
1801 entry[
"in_placements"] = json::array();
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 * 1
e-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]) > 1
e-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;
2221 refused = refused ||
row->sensitive || doctor.hasSensitive(
row->node->GetVolume());
2222 approximate = approximate ||
row->approximateExtent;
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
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
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