30#include "TGeoManager.h"
31#include "TGeoMatrix.h"
34#include "TGeoVolume.h"
37#include <nlohmann/json.hpp>
56 std::string topVolume;
58 std::vector<std::string> injections;
60 bool rootCheck =
false;
62 double rootOvlp = 0.001;
63 bool selfTest =
false;
64 bool listPairs =
false;
67void usage(
const char* argv0)
70 <<
"usage: " << argv0 <<
" --geometry <geom.root> [options]\n"
71 <<
" or: " << argv0 <<
" --self-test\n\n"
72 <<
" --geometry PATH ROOT file holding a TGeoManager (as written by geom.C's\n"
73 <<
" build_and_export, or by any other producer)\n"
74 <<
" --top NAME volume whose daughters are censused (default: the top volume)\n"
75 <<
" --points N boundary points sampled per solid (default 20000). Coverage only:\n"
76 <<
" every individual answer is exact, so this bounds false NEGATIVES\n"
77 <<
" --tol CM depth below which a containment is a shared face, not an overlap\n"
78 <<
" (default 1e-6)\n"
79 <<
" --residual CM a sampled point further than this from its own solid's boundary is\n"
80 <<
" discarded rather than used as evidence (default 1e-6)\n"
81 <<
" --pad CM bounding-box inflation for the pairwise rejection (default 0.1).\n"
82 <<
" Scopes which DISJOINT pairs get measured; never hides an overlap\n"
83 <<
" --volume-samples N Monte-Carlo estimate of the shared volume of each illegal pair\n"
84 <<
" --inject NAME:DX,DY,DZ translate a node by (dx,dy,dz) cm before the census. The\n"
85 <<
" positive control; may be repeated\n"
86 <<
" --root-check [N] also run TGeoManager::CheckOverlaps for comparison, optionally\n"
87 <<
" after SetNmeshPoints(N)\n"
88 <<
" --root-ovlp CM the tolerance handed to CheckOverlaps (default 0.001)\n"
89 <<
" --list-pairs print every tested pair, not only the illegal ones\n"
90 <<
" --json PATH write the census as JSON\n"
91 <<
" --self-test analytic controls, no geometry file needed; exits non-zero on any\n"
93 <<
"Exit code is the number of illegal pairs, capped at 250; 251 on a usage or load error.\n";
96bool parseInjection(
const std::string& spec, std::string&
name,
double shift[3])
98 const auto colon = spec.rfind(
':');
99 if (colon == std::string::npos) {
102 name = spec.substr(0, colon);
103 return std::sscanf(spec.c_str() + colon + 1,
"%lf,%lf,%lf", &shift[0], &shift[1], &shift[2]) == 3;
107bool injectShift(TGeoVolume* mother,
const std::string& nodeName,
const double shift[3])
111 if (nodeName !=
node->GetVolume()->GetName() && nodeName !=
node->GetName()) {
114 auto* nodeWithMatrix =
dynamic_cast<TGeoNodeMatrix*
>(
node);
115 if (nodeWithMatrix ==
nullptr) {
118 auto* replacement =
new TGeoHMatrix(*
node->GetMatrix());
119 const double* translation = replacement->GetTranslation();
120 replacement->SetDx(translation[0] + shift[0]);
121 replacement->SetDy(translation[1] + shift[1]);
122 replacement->SetDz(translation[2] + shift[2]);
123 replacement->RegisterYourself();
124 nodeWithMatrix->SetMatrix(replacement);
132 std::printf(
"\n%d placed solids -> %d pairs; %d survive the bounding-box rejection (%.2f %%)\n", census.
nSolids,
135 std::printf(
"disjoint %d | touching %d | INTERPENETRATING %d | contained %d | extruding %d (%.1f s)\n",
138 std::printf(
"points rejected as not on their own solid: %d; worst accepted residual %.3e cm\n",
141 std::printf(
"\n%-28s %-10s %10s %10s %8s %8s %6s\n",
"solid",
"shape",
"requested",
"accepted",
"rejected",
142 "residual",
"onSeg");
143 for (
const auto& solid : census.solids) {
144 std::printf(
"%-28s %-10s %10d %10d %8d %8.1e %6s\n", solid.name.c_str(),
145 solid.shapeClass.size() > 10 ? solid.shapeClass.substr(solid.shapeClass.size() - 10).c_str()
146 : solid.shapeClass.c_str(),
147 solid.requested, solid.accepted, solid.rejected, solid.worstResidualCm,
148 solid.usedPointsOnSegments ?
"yes" :
"no");
151 std::printf(
"\n%-46s %-17s %13s %8s %8s %13s\n",
"pair",
"verdict",
"depth(cm)",
"AinB",
"BinA",
"sep/vol");
152 for (
const auto& pair : census.pairs) {
154 pair.verdict == OverlapVerdict::Interpenetrating || pair.verdict == OverlapVerdict::Contained;
155 if (!listPairs && !illegal) {
159 std::snprintf(
label,
sizeof(
label),
"%s | %s", pair.nameA.c_str(), pair.nameB.c_str());
160 char trailer[64] =
"";
161 if (pair.sharedVolumeCm3 >= 0.) {
162 std::snprintf(trailer,
sizeof(trailer),
"V=%.4e", pair.sharedVolumeCm3);
163 }
else if (pair.separationCm >= 0.) {
164 std::snprintf(trailer,
sizeof(trailer),
"gap=%.4e", pair.separationCm);
167 pair.deepPointsAInsideB, pair.deepPointsBInsideA, trailer);
169 for (
const auto& pair : census.extrusions) {
170 std::printf(
"%-46s %-17s %13.6e %8d %8s %13s\n", (pair.nameA +
" extrudes " + pair.nameB).c_str(),
"EXTRUSION",
171 pair.depthCm, pair.deepPointsAInsideB,
"-",
"");
178 out[
"nSolids"] = census.
nSolids;
190 for (
const auto& solid : census.solids) {
191 out[
"solids"].push_back({{
"name", solid.name},
192 {
"shape", solid.shapeClass},
193 {
"requested", solid.requested},
194 {
"accepted", solid.accepted},
195 {
"rejected", solid.rejected},
196 {
"worstResidualCm", solid.worstResidualCm},
197 {
"usedPointsOnSegments", solid.usedPointsOnSegments}});
200 return nlohmann::json{{
"a", pair.nameA},
203 {
"depthCm", pair.depthCm},
204 {
"deepestPoint", pair.deepestPoint},
205 {
"deepestPointFrom", pair.deepestPointFrom},
206 {
"pointsAInsideB", pair.pointsAInsideB},
207 {
"pointsBInsideA", pair.pointsBInsideA},
208 {
"deepPointsAInsideB", pair.deepPointsAInsideB},
209 {
"deepPointsBInsideA", pair.deepPointsBInsideA},
210 {
"sampledA", pair.sampledA},
211 {
"sampledB", pair.sampledB},
212 {
"separationCm", pair.separationCm},
213 {
"sharedVolumeCm3", pair.sharedVolumeCm3},
214 {
"sharedVolumeErrCm3", pair.sharedVolumeErrCm3},
215 {
"sharedVolumeHits", pair.sharedVolumeHits}};
217 for (
const auto& pair : census.pairs) {
218 out[
"pairs"].push_back(pairJson(pair));
220 for (
const auto& pair : census.extrusions) {
221 out[
"extrusions"].push_back(pairJson(pair));
234void check(
bool condition,
const std::string& what)
239 std::printf(
" FAIL %s\n", what.c_str());
241 std::printf(
" ok %s\n", what.c_str());
245TGeoVolume* makeWorld(
const char*
name)
247 auto* manager =
new TGeoManager(
name,
name);
248 auto* material =
new TGeoMaterial(
"vac", 0., 0., 0.);
249 auto* medium =
new TGeoMedium(
"vac", 1, material);
250 auto* world = manager->MakeBox(
"world", medium, 100., 100., 100.);
251 manager->SetTopVolume(world);
260 const double faces[6][3] = {{1., 0., 0.}, {-1., 0., 0.}, {0., 1., 0.}, {0., -1., 0.}, {0., 0., 1.}, {0., 0., -1.}};
261 const double half[3] = {halfX, halfY, halfZ};
262 for (
const auto& normal : faces) {
263 const int axis = (normal[0] != 0.) ? 0 : ((normal[1] != 0.) ? 1 : 2);
264 const int axisU = (axis + 1) % 3;
265 const int axisV = (axis + 2) % 3;
270 directionU[axisU] = 1.;
271 directionV[axisV] = 1.;
273 const double sign = normal[axis];
274 std::vector<O2BVHSurfaceSolid::Point2D> wire;
275 const double extentU =
half[axisU];
276 const double extentV =
half[axisV];
278 wire = {{-extentU, -extentV}, {extentU, -extentV}, {extentU, extentV}, {-extentU, extentV}};
280 wire = {{-extentU, -extentV}, {-extentU, extentV}, {extentU, extentV}, {extentU, -extentV}};
282 solid->AddPlanarSurface(
origin, directionU, directionV, wire, {});
284 solid->CloseShape(
false);
290 std::printf(
"== o2-bench-cadsupport-overlap self-test ==\n");
298 TGeoVolume* world = makeWorld(
"touch");
299 auto*
left =
new TGeoVolume(
"left", makeSurfaceBox(
"leftBox", 1., 1., 1.), world->GetMedium());
300 auto*
right =
new TGeoVolume(
"right", makeSurfaceBox(
"rightBox", 1., 1., 1.), world->GetMedium());
301 world->AddNode(
left, 1,
new TGeoTranslation(-1., 0., 0.));
302 world->AddNode(
right, 1,
new TGeoTranslation(1., 0., 0.));
303 gGeoManager->CloseGeometry();
305 check(census.
nPairsTested == 1,
"touching: the pair survives the box rejection");
307 "touching: a shared face is TOUCHING, not an overlap");
308 check(!census.
pairs.empty() && census.
pairs[0].pointsAInsideB + census.
pairs[0].pointsBInsideA > 0,
309 "touching: the check was capable of firing (points ARE found inside)");
311 "touching: the depth is at the tolerance, i.e. zero");
317 TGeoVolume* world = makeWorld(
"overlap");
318 auto*
left =
new TGeoVolume(
"left", makeSurfaceBox(
"leftBox", 1., 1., 1.), world->GetMedium());
319 auto*
right =
new TGeoVolume(
"right", makeSurfaceBox(
"rightBox", 1., 1., 1.), world->GetMedium());
320 world->AddNode(
left, 1,
new TGeoTranslation(-1., 0., 0.));
321 world->AddNode(
right, 1,
new TGeoTranslation(0.8, 0., 0.));
322 gGeoManager->CloseGeometry();
327 const double depth = census.
pairs.empty() ? 0. : census.
pairs[0].depthCm;
329 "injected 0.2 cm: the depth is the injected displacement (" +
std::to_string(
depth) +
")");
330 const double volume = census.
pairs.empty() ? -1. : census.
pairs[0].sharedVolumeCm3;
331 check(std::abs(volume - 0.8) < 0.02,
332 "injected 0.2 cm: shared volume 0.2 x 2 x 2 = 0.8 cm3 (" +
std::to_string(volume) +
")");
338 TGeoVolume* world = makeWorld(
"gap");
339 auto*
left =
new TGeoVolume(
"left", makeSurfaceBox(
"leftBox", 1., 1., 1.), world->GetMedium());
340 auto*
right =
new TGeoVolume(
"right", makeSurfaceBox(
"rightBox", 1., 1., 1.), world->GetMedium());
341 world->AddNode(
left, 1,
new TGeoTranslation(-1., 0., 0.));
342 world->AddNode(
right, 1,
new TGeoTranslation(1.2, 0., 0.));
343 gGeoManager->CloseGeometry();
346 const double separation = census.
pairs.empty() ? -1. : census.
pairs[0].separationCm;
347 check(std::abs(separation - 0.2) < 1
e-9,
348 "0.2 cm gap: the separation is recovered (" +
std::to_string(separation) +
")");
354 TGeoVolume* world = makeWorld(
"thin");
355 auto*
left =
new TGeoVolume(
"left", makeSurfaceBox(
"leftBox", 1., 1., 1.), world->GetMedium());
356 auto*
right =
new TGeoVolume(
"right", makeSurfaceBox(
"rightBox", 1., 1., 1.), world->GetMedium());
357 world->AddNode(
left, 1,
new TGeoTranslation(-1., 0., 0.));
358 world->AddNode(
right, 1,
new TGeoTranslation(1. - 1
e-5, 0., 0.));
359 gGeoManager->CloseGeometry();
361 check(loose.
nInterpenetrating == 1,
"1e-5 cm interpenetration is resolved at the default 1e-6 tolerance");
366 "CONTROL: at a 1e-4 tolerance the same 1e-5 interpenetration reads as touching");
372 TGeoVolume* world = makeWorld(
"nested");
373 auto* outer =
new TGeoVolume(
"outer", makeSurfaceBox(
"outerBox", 3., 3., 3.), world->GetMedium());
374 auto* inner =
new TGeoVolume(
"inner", makeSurfaceBox(
"innerBox", 1., 1., 1.), world->GetMedium());
375 world->AddNode(outer, 1,
new TGeoTranslation(0., 0., 0.));
376 world->AddNode(inner, 1,
new TGeoTranslation(0., 0., 0.));
377 gGeoManager->CloseGeometry();
379 check(census.
nContained == 1,
"a solid wholly inside another is CONTAINED, not merely overlapping");
386 TGeoVolume* world = makeWorld(
"press");
387 auto* pin =
new TGeoVolume(
"pin",
new TGeoTube(
"pinTube", 0., 1., 5.), world->GetMedium());
388 auto* sleeve =
new TGeoVolume(
"sleeve",
new TGeoTube(
"sleeveTube", 1., 2., 5.), world->GetMedium());
389 world->AddNode(pin, 1,
new TGeoTranslation(0., 0., 0.));
390 world->AddNode(sleeve, 1,
new TGeoTranslation(0., 0., 0.));
391 gGeoManager->CloseGeometry();
394 "an exact press fit on a cylinder is TOUCHING, not an 8.6e-3 cm overlap");
395 const double depth = census.
pairs.empty() ? -1. : census.
pairs[0].depthCm;
402 auto*
box =
new TGeoBBox(
"residualBox", 1., 1., 1.);
403 std::vector<double> points;
407 check(accepted > 0 && rejected == 0,
"TGeoBBox: every sampled point is on the box");
408 check(worst < 1
e-9,
"TGeoBBox: worst accepted residual " +
std::to_string(worst) +
" is at round-off");
413 auto* solid = makeSurfaceBox(
"contractBox", 1., 2., 3.);
414 const int meshVertices = solid->GetNmeshVertices();
415 std::vector<double>
buffer(3 * (meshVertices + 5000), -1.2345e33);
416 check(solid->GetPointsOnSegments(meshVertices + 5000,
buffer.data()),
417 "GetPointsOnSegments fills the buffer when asked for more than the mesh");
427 check(unfilled == 0,
"GetPointsOnSegments leaves no slot unwritten");
429 "every generated point is on the solid (worst " +
std::to_string(worst) +
")");
430 check(!solid->GetPointsOnSegments(meshVertices - 1,
buffer.data()),
431 "below the mesh size it declines, so ROOT falls back to the full exact vertex set");
435 std::printf(
"\n%d checks, %d failures\n", gChecks, gFailures);
436 return gFailures == 0 ? 0 : 1;
445 const std::string argument = argv[
index];
446 auto next = [&](
const char* what) -> std::string {
447 if (
index + 1 >= argc) {
448 std::cerr <<
"error: " << what <<
" needs a value\n";
451 return argv[++
index];
453 if (argument ==
"-h" || argument ==
"--help") {
456 }
else if (argument ==
"--geometry") {
457 options.geometry = next(
"--geometry");
458 }
else if (argument ==
"--top") {
459 options.topVolume = next(
"--top");
460 }
else if (argument ==
"--points") {
461 options.check.pointsPerSolid = std::atoi(next(
"--points").c_str());
462 }
else if (argument ==
"--tol") {
463 options.check.depthTolerance = std::atof(next(
"--tol").c_str());
464 }
else if (argument ==
"--residual") {
465 options.check.residualTolerance = std::atof(next(
"--residual").c_str());
466 }
else if (argument ==
"--pad") {
467 options.check.padCm = std::atof(next(
"--pad").c_str());
468 }
else if (argument ==
"--volume-samples") {
469 options.check.volumeSamples = std::atoi(next(
"--volume-samples").c_str());
470 }
else if (argument ==
"--inject") {
471 options.injections.push_back(next(
"--inject"));
472 }
else if (argument ==
"--root-check") {
473 options.rootCheck =
true;
474 if (
index + 1 < argc && argv[
index + 1][0] !=
'-') {
475 options.rootNmesh = std::atoi(argv[++
index]);
477 }
else if (argument ==
"--root-ovlp") {
478 options.rootOvlp = std::atof(next(
"--root-ovlp").c_str());
479 }
else if (argument ==
"--list-pairs") {
480 options.listPairs =
true;
481 }
else if (argument ==
"--json") {
482 options.json = next(
"--json");
483 }
else if (argument ==
"--self-test") {
484 options.selfTest =
true;
486 std::cerr <<
"error: unknown argument " << argument <<
"\n";
492 if (options.selfTest) {
495 if (options.geometry.empty()) {
500 TGeoManager::Import(options.geometry.c_str());
501 if (gGeoManager ==
nullptr) {
502 std::cerr <<
"error: no TGeoManager in " << options.geometry <<
"\n";
505 TGeoVolume*
top = options.topVolume.empty() ? gGeoManager->GetTopVolume()
506 : gGeoManager->GetVolume(options.topVolume.c_str());
507 if (
top ==
nullptr) {
508 std::cerr <<
"error: no such volume: " << options.topVolume <<
"\n";
512 for (
const auto& specification : options.injections) {
514 double shift[3] = {0., 0., 0.};
515 if (!parseInjection(specification,
name, shift)) {
516 std::cerr <<
"error: cannot parse --inject " << specification <<
" (expected NAME:DX,DY,DZ)\n";
519 if (!injectShift(
top,
name, shift)) {
520 std::cerr <<
"error: --inject names no daughter of " <<
top->GetName() <<
": " <<
name <<
"\n";
523 std::printf(
"# injected: %s by (%g, %g, %g) cm\n",
name.c_str(), shift[0], shift[1], shift[2]);
526 std::printf(
"# geometry %s, top volume %s, %d points per solid, depth tolerance %g cm, pad %g cm\n",
527 options.geometry.c_str(),
top->GetName(), options.check.pointsPerSolid, options.check.depthTolerance,
528 options.check.padCm);
531 printCensus(census, options.listPairs);
533 if (options.rootCheck) {
534 std::printf(
"\n== TGeoManager::CheckOverlaps, for comparison (nmesh %s, ovlp %g) ==\n",
535 options.rootNmesh > 0 ?
std::to_string(options.rootNmesh).c_str() :
"default", options.rootOvlp);
536 gGeoManager->GetGeomPainter();
537 if (options.rootNmesh > 0) {
538 gGeoManager->SetNmeshPoints(options.rootNmesh);
540 gGeoManager->CheckOverlaps(options.rootOvlp);
541 gGeoManager->PrintOverlaps();
544 if (!options.json.empty()) {
545 nlohmann::json out = censusToJson(census);
546 out[
"geometry"] = options.geometry;
547 out[
"top"] =
top->GetName();
548 out[
"options"] = {{
"pointsPerSolid", options.check.pointsPerSolid},
549 {
"depthToleranceCm", options.check.depthTolerance},
550 {
"residualToleranceCm", options.check.residualTolerance},
551 {
"padCm", options.check.padCm},
552 {
"volumeSamples", options.check.volumeSamples}};
553 out[
"injections"] = options.injections;
554 std::ofstream
stream(options.json);
555 stream << out.dump(1) <<
"\n";
556 std::printf(
"\nwrote %s\n", options.json.c_str());
header::DataOrigin origin
std::unique_ptr< expressions::Node > node
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
std::array< double, 3 > Point3D
static constexpr double kSurfacePointTolerance
GLdouble GLdouble GLdouble GLdouble top
GLuint const GLchar * name
GLuint GLsizei const GLchar * label
GLint GLint GLsizei GLsizei GLsizei depth
GLsizei const GLint * box
GLsizeiptr const void GLenum usage
OverlapCensus CheckWorldOverlaps(const TGeoVolume *volume, const OverlapOptions &options=OverlapOptions())
Census every pair of volume's immediate daughters, and optionally each daughter against volume.
const char * OverlapVerdictName(OverlapVerdict verdict)
int SampleBoundaryPoints(const TGeoShape *shape, int npoints, double residualTolerance, std::vector< double > &points, int &rejected, double &worstResidual, bool *usedPointsOnSegments=nullptr)
void check(const std::vector< std::string > &arguments, const std::vector< ConfigParamSpec > &workflowOptions, const std::vector< DeviceSpec > &deviceSpecs, CheckMatrix &matrix)
std::string to_string(gsl::span< T, Size > span)
int nPairsTotal
N (N - 1) / 2.
int illegalCount() const
The one-line answer: nInterpenetrating + nContained + nExtruding.
int nPairsTested
after the bounding-box rejection
std::vector< OverlapPair > pairs
only the pairs that survived the bounding-box rejection
double depthTolerance
A containment shallower than this is a shared boundary, not an overlap. In cm.
int volumeSamples
Monte-Carlo samples for the shared volume of an illegal pair; 0, the default, disables the estimate.
One pair of placed solids, and everything measured about it.