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) < 1e-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. - 1e-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 < 1e-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
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.