61#include "TGeoManager.h"
62#include "TGeoMaterial.h"
63#include "TGeoMatrix.h"
64#include "TGeoMedium.h"
65#include "TGeoNavigator.h"
67#include "TGeoSphere.h"
69#include "TGeoVolume.h"
71#include <nlohmann/json.hpp>
87using json = nlohmann::json;
99constexpr int kXRayFormatVersion = 2;
103 return json{{
"rays",
c.rays},
104 {
"raysIdentical",
c.raysIdentical},
105 {
"raysStructural",
c.raysStructural},
106 {
"matched",
c.matched},
107 {
"displacedCrossings",
c.displaced},
108 {
"missingCrossings",
c.missing},
109 {
"extraCrossings",
c.extra},
110 {
"kindMismatch",
c.kindMismatch},
111 {
"worstDeltaT",
c.worstDeltaT},
112 {
"worstOrigin", {
c.worstOrigin[0],
c.worstOrigin[1],
c.worstOrigin[2]}},
113 {
"worstDir", {
c.worstDir[0],
c.worstDir[1],
c.worstDir[2]}},
114 {
"worstReason",
c.worstReason}};
119 return json{{
"rays",
r.rays},
120 {
"raysWithCrossings",
r.raysWithCrossings},
121 {
"crossings",
r.crossings},
123 {
"zeroLengthSteps",
r.zeroLengthSteps},
124 {
"nonAdvancingSteps",
r.nonAdvancingSteps},
125 {
"unstickPushes",
r.unstickPushes},
126 {
"iterationCapHits",
r.iterationCapHits},
127 {
"unterminated",
r.unterminated},
128 {
"oddCrossingLists",
r.oddCrossingLists},
129 {
"nonAlternating",
r.nonAlternating},
130 {
"duplicateCrossings",
r.duplicateCrossings},
131 {
"parityMismatchIntervals",
r.parityMismatchIntervals},
132 {
"parityMismatchNearBoundary",
r.parityMismatchNearBoundary},
133 {
"originInside",
r.originInside},
134 {
"boundaryWithoutTransition",
r.boundaryWithoutTransition},
135 {
"originOutsideWorld",
r.originOutsideWorld},
136 {
"insideLengthCm",
r.insideLength},
137 {
"seconds",
r.seconds}};
149class NavigatorTransport
164 NavigatorTransport(TGeoManager* manager, TGeoShape* shape,
const Point3D& bboxMin,
165 const Point3D& bboxMax,
const TGeoMatrix* placement =
nullptr)
168 auto* material =
new TGeoMaterial(
"Vacuum", 0., 0., 0.);
169 auto* medium =
new TGeoMedium(
"Vacuum", 1, material);
172 for (
int k = 0; k < 3; ++k) {
173 centre[k] = 0.5 * (bboxMax[k] + bboxMin[k]);
174 half[k] = 0.5 * (bboxMax[k] - bboxMin[k]) + 0.05 * (bboxMax[k] - bboxMin[k]) + 0.1;
177 mWorld =
new TGeoVolume(
"TOP", worldBox, medium);
178 mPart =
new TGeoVolume(
"PART", shape, medium);
180 mWorld->AddNode(mPart, 1, placement !=
nullptr ?
new TGeoHMatrix(*placement) :
nullptr);
181 mManager->SetTopVolume(mWorld);
182 mManager->CloseGeometry();
183 mManager->SetNsegments(80);
184 mNavigator = mManager->GetCurrentNavigator();
188 ~NavigatorTransport() =
default;
190 NavigatorTransport(
const NavigatorTransport&) =
delete;
191 NavigatorTransport& operator=(
const NavigatorTransport&) =
delete;
193 bool valid()
const {
return mNavigator !=
nullptr; }
198 std::vector<Crossing> crossings;
199 mNavigator->InitTrack(
origin.data(), dir.data());
200 if (mNavigator->IsOutside()) {
204 ++stats.originOutsideWorld;
207 bool inPart = (mNavigator->GetCurrentVolume() == mPart);
209 ++stats.originInside;
212 for (; iter < cfg.
maxIter; ++iter) {
213 const double* before = mNavigator->GetCurrentPoint();
215 for (
int k = 0; k < 3; ++k) {
216 tBefore += (before[k] -
origin[k]) * dir[k];
218 mNavigator->FindNextBoundaryAndStep(TGeoShape::Big(), kFALSE);
219 const double step = mNavigator->GetStep();
221 const double tCross = tBefore + step;
223 ++stats.zeroLengthSteps;
225 if (!(tCross > tBefore)) {
226 ++stats.nonAdvancingSteps;
228 if (mNavigator->IsOutside() || tCross > tMax || !(step < TGeoShape::Big())) {
231 const bool nowIn = (mNavigator->GetCurrentVolume() == mPart);
232 if (nowIn != inPart) {
233 crossings.push_back({tCross, nowIn ? +1 : -1});
236 ++stats.boundaryWithoutTransition;
240 ++stats.iterationCapHits;
243 ++stats.unterminated;
249 TGeoManager* mManager =
nullptr;
250 TGeoVolume* mWorld =
nullptr;
251 TGeoVolume* mPart =
nullptr;
252 TGeoNavigator* mNavigator =
nullptr;
276 std::string explicitSurfaces;
277 std::string explicitFacets;
278 std::string explicitShape;
279 std::string explicitFlatCSG;
283 int flatSplitDepth = -1;
284 double flatMinBoxFraction = -1.;
285 std::string partsPattern;
287 std::string axesSpec =
"xyz";
292 double margin = 1.e-3;
296 double tiltDegrees = 0.;
301 std::string dumpRays;
302 std::string refCrossings;
307 std::set<std::string> representations = {
"surface",
"mesh",
"shape"};
308 bool skipNavigator =
false;
309 bool selfTest =
false;
314 int perfPoints = 4096;
319 std::string ladderSpec;
326 std::string surfaces;
335const std::string& sourceFor(
const Part& part,
const std::string&
name)
337 if (
name ==
"surface") {
338 return part.surfaces;
340 if (
name ==
"mesh") {
343 if (
name ==
"flatcsg") {
350const std::array<std::string, 4>& allRepresentations()
352 static const std::array<std::string, 4> names{
"surface",
"mesh",
"shape",
"flatcsg"};
356std::string deriveSidecarPath(
const std::string& surfacesPath,
const char* prefixOut,
357 const char* suffixOut)
359 const auto slash = surfacesPath.find_last_of(
'/');
360 const std::string dir = slash == std::string::npos ? std::string() : surfacesPath.substr(0, slash + 1);
361 std::string base = slash == std::string::npos ? surfacesPath : surfacesPath.substr(slash + 1);
362 const std::string prefix =
"surfaces_";
363 const std::string suffix =
".bin";
364 if (base.rfind(prefix, 0) != 0 || base.size() <= prefix.size() + suffix.size() ||
365 base.compare(base.size() - suffix.size(), suffix.size(), suffix) != 0) {
368 const std::string stem = base.substr(prefix.size(), base.size() - prefix.size() - suffix.size());
369 return dir + prefixOut + stem + suffixOut;
377 std::ifstream probe(
path);
378 return static_cast<bool>(probe);
382std::string sanitizePartId(
const std::string&
id)
385 out.reserve(
id.
size());
386 for (
const char c :
id) {
387 out.push_back((std::isalnum(
static_cast<unsigned char>(
c)) ||
c ==
'-' ||
c ==
'.') ?
c :
'_');
392void printUsage(
const char* argv0)
394 std::cout <<
"X-ray / geantino transport benchmark -- ordered crossing lists, by stepping.\n\n"
396 << argv0 <<
" --db <dir> [--parts <substring>] [--raster N] [--axes xyz]\n"
397 " [--dump-rays D] [--ref-crossings D] [--json out.json]\n"
399 << argv0 <<
" --surfaces <f> [--facets <f>] [--shape <f>] [--flatcsg <f>]\n"
400 " [options as above]\n"
402 << argv0 <<
" --self-test\n\n"
403 " --raster N N x N parallel rays per beam axis (default 48). Structured, not random:\n"
404 " the chord integral converges as the boundary-cell count (~N) rather than\n"
405 " as sqrt of the sample count, and a lattice generates the edge-on and\n"
406 " vertex-on rays that stall a transport loop.\n"
407 " --axes xyz which beam axes to fire (subset of x,y,z; default all three)\n"
408 " --beams N fire N Fibonacci-spiral directions instead of the axis beams. A parallel\n"
409 " beam is DIRECTION-POOR: three axes are three directions however many rays\n"
410 " are fired, and a direction-dependent defect (the torus quartic) is\n"
411 " invisible to them. Use this whenever hunting one.\n"
412 " --tilt DEG rotate every beam off its axis by DEG (default 0). An axis-aligned beam\n"
413 " is a special family of configurations; a tilted one is generic. The known\n"
414 " torus quartic defect is invisible at tilt 0 and visible at tilt 12.\n"
415 " --dump-rays D write D/xrays_<part>.json (the raster window and every ray) and exit\n"
416 " --ref-crossings D read D/crossings_<part>.json (Detectors/CADSupport/validation/xrayOracle.py) and score\n"
417 " the crossing LISTS against it, per representation, per mode\n"
418 " --flatcsg <f> an o2::cad::O2FlatCSG sidecar (flatcsg_*.bin) as its own subject.\n"
419 " NOT in the default set -- a flat part's shape_*.root already holds\n"
420 " the same solid -- so name it here, or in --representations.\n"
421 " This is how the flat halfspace solid is scored against the SAME part\n"
422 " emitted as a plain TGeoCompositeShape through --shape: two subjects,\n"
423 " one raster, one sample set (Design_FlatCSGSolid.md section 9).\n"
424 " --flat-split-depth N override O2FlatCSG::SetSplitDepth on every flat subject\n"
425 " --flat-min-box-fraction X override O2FlatCSG::SetMinBoxFraction likewise. The two\n"
426 " knobs are swept from here rather than from a test, so the defaults in\n"
427 " the header rest on the same instrument that reports the query cost.\n"
428 " --representations surface,mesh,shape,flatcsg which to run (default: all present)\n"
429 " --no-navigator skip mode (b); mode (a) depends on nothing but the shape\n"
430 " --perf the representation cost/memory comparison: per-call ns for Contains,\n"
431 " Safety, DistFromOutside and DistFromInside, plus transport ns/ray and\n"
432 " ns/crossing, plus structural and measured memory -- for every\n"
433 " representation, from ONE shared sample set per part. Warm cache; the\n"
434 " reported number is the median over --perf-passes complete passes and the\n"
435 " min/max spread is printed with it.\n"
436 " --perf-points N query points per part (default 4096)\n"
437 " --perf-rays N rays per distance kernel (default 4096)\n"
438 " --perf-passes N timed passes (default 9); --perf-warmup N untimed first (default 2)\n"
439 " --ladder 2,4,8 the synthetic boolean ladder: unions of K TGeoTubes as a left-deep CHAIN\n"
440 " and as a BALANCED tree, timed with the same kernels. Needs no database:\n"
441 " every genuine boolean in the corpus is a 2-leaf union, so the corpus\n"
442 " cannot answer how a composite scales with leaf count and this fixture is\n"
444 " --push X distance advanced past a found crossing (cm, default 1e-9 = kRayTolerance)\n"
445 " --unstick-push X the nudge a stalled step is repaired with (cm, default 1e-6); every use\n"
446 " is counted in `unstickPushes`\n"
447 " --max-iter N transport iteration cap per ray (default 512)\n"
448 " --self-test analytic self-checks (box, tube, sphere) plus the synthetic controls that\n"
449 " prove the comparison can fail. Needs no database and no oracle.\n\n"
450 "Three-stage round trip:\n"
452 << argv0 <<
" --db <db> --dump-rays /tmp/x\n"
453 " xrayOracle.py --brep <part>.brep --rays /tmp/x/xrays_<part>.json \\\n"
454 " --out /tmp/x/crossings_<part>.json\n"
456 << argv0 <<
" --db <db> --ref-crossings /tmp/x --json /tmp/x/xray.json\n";
459std::set<std::string> splitCsv(
const std::string& s)
461 std::set<std::string> out;
462 std::stringstream ss(s);
464 while (std::getline(ss, tok,
',')) {
472bool parseArgs(
int argc,
char** argv,
Options& opt)
474 for (
int i = 1;
i < argc; ++
i) {
475 const std::string
a = argv[
i];
476 auto next = [&](
const char* flag) -> std::string {
478 throw std::runtime_error(std::string(
"missing value for ") + flag);
483 opt.db = next(
"--db");
484 }
else if (
a ==
"--surfaces") {
485 opt.explicitSurfaces = next(
"--surfaces");
486 }
else if (
a ==
"--facets") {
487 opt.explicitFacets = next(
"--facets");
488 }
else if (
a ==
"--shape") {
489 opt.explicitShape = next(
"--shape");
490 }
else if (
a ==
"--flatcsg") {
491 opt.explicitFlatCSG = next(
"--flatcsg");
492 }
else if (
a ==
"--flat-split-depth") {
493 opt.flatSplitDepth = std::stoi(next(
"--flat-split-depth"));
494 }
else if (
a ==
"--flat-min-box-fraction") {
495 opt.flatMinBoxFraction = std::stod(next(
"--flat-min-box-fraction"));
496 }
else if (
a ==
"--parts") {
497 opt.partsPattern = next(
"--parts");
498 }
else if (
a ==
"--raster") {
499 opt.raster = std::stoi(next(
"--raster"));
500 }
else if (
a ==
"--axes") {
501 opt.axesSpec = next(
"--axes");
502 }
else if (
a ==
"--beams") {
503 opt.fanBeams = std::stoi(next(
"--beams"));
504 }
else if (
a ==
"--tilt") {
505 opt.tiltDegrees = std::stod(next(
"--tilt"));
506 }
else if (
a ==
"--margin") {
507 opt.margin = std::stod(next(
"--margin"));
508 }
else if (
a ==
"--dump-rays") {
509 opt.dumpRays = next(
"--dump-rays");
510 }
else if (
a ==
"--ref-crossings") {
511 opt.refCrossings = next(
"--ref-crossings");
512 }
else if (
a ==
"--json") {
513 opt.jsonOut = next(
"--json");
514 }
else if (
a ==
"--representations") {
515 opt.representations = splitCsv(next(
"--representations"));
516 }
else if (
a ==
"--no-navigator") {
517 opt.skipNavigator =
true;
518 }
else if (
a ==
"--perf") {
520 }
else if (
a ==
"--perf-points") {
521 opt.perfPoints = std::stoi(next(
"--perf-points"));
522 }
else if (
a ==
"--perf-rays") {
523 opt.perfRays = std::stoi(next(
"--perf-rays"));
524 }
else if (
a ==
"--perf-passes") {
525 opt.perfPasses = std::stoi(next(
"--perf-passes"));
526 }
else if (
a ==
"--perf-warmup") {
527 opt.perfWarmup = std::stoi(next(
"--perf-warmup"));
528 }
else if (
a ==
"--ladder") {
529 opt.ladderSpec = next(
"--ladder");
530 }
else if (
a ==
"--push") {
531 opt.step.push = std::stod(next(
"--push"));
532 }
else if (
a ==
"--unstick-push") {
533 opt.step.unstickPush = std::stod(next(
"--unstick-push"));
534 }
else if (
a ==
"--zero-step") {
535 opt.step.zeroStep = std::stod(next(
"--zero-step"));
536 }
else if (
a ==
"--max-iter") {
537 opt.step.maxIter = std::stoi(next(
"--max-iter"));
538 }
else if (
a ==
"--self-test") {
540 }
else if (
a ==
"-h" ||
a ==
"--help") {
544 throw std::runtime_error(
"unrecognized option: " +
a);
547 if (!opt.selfTest && opt.ladderSpec.empty() && opt.db.empty() && opt.explicitSurfaces.empty() &&
548 opt.explicitShape.empty() && opt.explicitFlatCSG.empty()) {
549 throw std::runtime_error(
550 "either --db <dir>, --surfaces/--shape/--flatcsg <file>, "
551 "--ladder <counts> or --self-test is required");
554 if (!opt.explicitFlatCSG.empty()) {
555 opt.representations.insert(
"flatcsg");
560std::vector<Part> collectParts(
const Options& opt)
562 std::vector<Part> parts;
563 if (!opt.explicitSurfaces.empty() || !opt.explicitShape.empty() ||
564 !opt.explicitFlatCSG.empty()) {
565 Part part{
"adhoc",
"adhoc", opt.explicitSurfaces, opt.explicitFacets, opt.explicitShape,
566 opt.explicitFlatCSG};
570 if (!part.surfaces.empty()) {
571 if (part.facets.empty()) {
572 part.facets = deriveSidecarPath(part.surfaces,
"facets_",
".bin");
574 if (part.shape.empty()) {
575 part.shape = deriveSidecarPath(part.surfaces,
"shape_",
".root");
577 if (part.flatcsg.empty()) {
578 part.flatcsg = deriveSidecarPath(part.surfaces,
"flatcsg_",
".bin");
581 parts.push_back(std::move(part));
584 const std::string manifestPath = opt.db +
"/manifest.json";
585 std::ifstream in(manifestPath);
587 throw std::runtime_error(
"cannot open " + manifestPath);
591 for (
const auto& p : manifest.at(
"parts")) {
593 part.id = p.at(
"id").get<std::string>();
594 part.model = p.value(
"model", std::string(
"?"));
595 part.surfaces = p.value(
"surfaces", std::string());
596 part.facets = p.value(
"facets", std::string());
597 part.shape = p.value(
"shape", std::string());
598 if (part.shape.empty()) {
599 part.shape = deriveSidecarPath(part.surfaces,
"shape_",
".root");
601 part.flatcsg = p.value(
"flatcsg", std::string());
602 if (part.flatcsg.empty()) {
603 part.flatcsg = deriveSidecarPath(part.surfaces,
"flatcsg_",
".bin");
605 if (!opt.partsPattern.empty()) {
606 const bool idMatch = part.id.find(opt.partsPattern) != std::string::npos;
607 const bool modelMatch = part.model.find(opt.partsPattern) != std::string::npos;
608 if (!idMatch && !modelMatch) {
612 parts.push_back(std::move(part));
617void writeRays(
const std::string& dir,
const std::string& partId,
const Raster& raster,
618 const std::string& bboxSource)
621 doc[
"version"] = kXRayFormatVersion;
622 doc[
"part"] = partId;
625 doc[
"raster"] = raster.
n;
626 json beams = json::array();
627 for (
const auto& beam : raster.
beams) {
628 beams.push_back({{
"label", beam.label}, {
"dir", {beam.dir[0], beam.dir[1], beam.dir[2]}}});
630 doc[
"beams"] = beams;
634 doc[
"bboxSource"] = bboxSource;
635 json rays = json::array();
636 for (
const auto&
r : raster.
rays) {
637 rays.push_back({{
"o", {
r.origin[0],
r.origin[1],
r.origin[2]}},
638 {
"d", {
r.dir[0],
r.dir[1],
r.dir[2]}},
642 doc[
"rays"] = std::move(rays);
643 const std::string
path = dir +
"/xrays_" + sanitizePartId(partId) +
".json";
644 std::ofstream out(
path);
646 throw std::runtime_error(
"cannot write " +
path);
649 std::printf(
" wrote %s (%zu rays)\n",
path.c_str(), raster.
rays.size());
653struct OracleCrossings {
655 double tolerance = 1.e-7;
656 double capacity = 0.;
657 double volumeChord = 0.;
659 std::vector<std::vector<Crossing>> perRay;
660 std::vector<bool> ambiguous;
661 long long ambiguousRays = 0;
665OracleCrossings loadOracleCrossings(
const std::string& dir,
const std::string& partId)
668 const std::string
path = dir +
"/crossings_" + sanitizePartId(partId) +
".json";
669 std::ifstream in(
path);
675 if (doc.value(
"version", 0) != kXRayFormatVersion) {
676 throw std::runtime_error(
path +
": unsupported format version");
679 out.tolerance = doc.value(
"tolerance", 1.e-7);
680 out.capacity = doc.value(
"capacity", 0.);
681 out.volumeChord = doc.value(
"volumeChord", 0.);
682 out.valid = doc.value(
"valid",
false);
683 out.ambiguousRays = doc.value(
"ambiguousRays", 0);
684 out.raster.n = doc.value(
"raster", 0);
685 out.raster.transverseMargin = doc.value(
"transverseMargin", 0.);
686 const auto& window0 = doc.at(
"windowMin");
687 const auto& window1 = doc.at(
"windowMax");
688 for (
int k = 0; k < 3; ++k) {
689 out.raster.windowMin[k] = window0[k].get<
double>();
690 out.raster.windowMax[k] = window1[k].get<
double>();
692 out.raster.cellArea = doc.at(
"cellArea").get<std::vector<double>>();
693 out.raster.windowExcess = doc.value(
"windowExcess", std::vector<double>(out.raster.cellArea.size(), 0.));
694 for (
const auto&
b : doc.at(
"beams")) {
696 beam.
label =
b.at(
"label").get<std::string>();
697 for (
int k = 0; k < 3; ++k) {
698 beam.
dir[k] =
b.at(
"dir")[k].get<
double>();
700 out.raster.beams.push_back(std::move(beam));
702 out.raster.rays.reserve(doc.at(
"rays").size());
703 for (
const auto&
r : doc.at(
"rays")) {
705 for (
int k = 0; k < 3; ++k) {
706 ray.
origin[k] =
r.at(
"o")[k].get<
double>();
707 ray.
dir[k] =
r.at(
"d")[k].get<
double>();
709 ray.
tMax =
r.at(
"tmax").get<
double>();
710 ray.
beam =
r.at(
"beam").get<
int>();
711 out.raster.rays.push_back(ray);
712 std::vector<Crossing> crossings;
713 const auto& ts =
r.at(
"t");
714 const auto& kinds =
r.at(
"k");
715 for (
size_t i = 0;
i < ts.size(); ++
i) {
716 crossings.push_back({ts[
i].get<
double>(), kinds[
i].get<int>()});
718 out.perRay.push_back(std::move(crossings));
722 out.ambiguous.push_back(
r.value(
"amb",
false));
736 if (placement ==
nullptr) {
741 placement->MasterToLocal(
origin.data(), localOrigin.data());
742 placement->MasterToLocalVect(dir.data(), localDir.data());
750 const double half[3] = {
box.GetDX(),
box.GetDY(),
box.GetDZ()};
751 for (
int k = 0; k < 3; ++k) {
752 lo[k] =
box.GetOrigin()[k] -
half[k];
753 hi[k] =
box.GetOrigin()[k] +
half[k];
755 if (placement ==
nullptr) {
758 Point3D outLo{1.e300, 1.e300, 1.e300};
759 Point3D outHi{-1.e300, -1.e300, -1.e300};
760 for (
int corner = 0; corner < 8; ++corner) {
761 const double local[3] = {(corner & 1) ? hi[0] : lo[0], (corner & 2) ? hi[1] : lo[1],
762 (corner & 4) ? hi[2] : lo[2]};
764 placement->LocalToMaster(local, master);
765 for (
int k = 0; k < 3; ++k) {
766 outLo[k] = std::min(outLo[k], master[k]);
767 outHi[k] = std::max(outHi[k], master[k]);
783 const std::string&
path;
785 const Candidate candidates[4] = {{
"shape", part.shape},
786 {
"flatcsg", part.flatcsg},
787 {
"mesh", part.facets},
788 {
"surface", part.surfaces}};
789 for (
const auto& candidate : candidates) {
790 if (!opt.representations.count(candidate.name) || !
fileExists(candidate.path)) {
793 auto* manager =
new TGeoManager(
"xrayBBox",
"bbox probe");
794 TGeoShape* shape =
nullptr;
795 std::unique_ptr<TGeoHMatrix> placement;
796 if (std::string(candidate.name) ==
"surface") {
799 solid->CloseShape(
true);
802 }
else if (std::string(candidate.name) ==
"mesh") {
808 }
else if (std::string(candidate.name) ==
"flatcsg") {
809 auto* solid =
new O2FlatCSG(part.id.c_str());
821 const auto*
box =
dynamic_cast<const TGeoBBox*
>(shape);
822 if (
box !=
nullptr) {
823 placedBox(*
box, placement.get(), lo, hi);
826 gGeoManager =
nullptr;
830 gGeoManager =
nullptr;
870 TGeoManager* manager =
nullptr;
871 TGeoShape* shape =
nullptr;
874 std::unique_ptr<TGeoHMatrix> placement;
878 double loadSeconds = 0.;
879 double closeSeconds = 0.;
880 bool meshClosedBody =
true;
889LoadedRep loadRepresentation(
const std::string&
name,
const std::string&
source,
890 const std::string& partId,
int flatSplitDepth = -1,
891 double flatMinBoxFraction = -1.)
894 rep.manager =
new TGeoManager((
"perf_" +
name).c_str(),
"representation benchmark");
897 const auto t0 = std::chrono::steady_clock::now();
898 if (
name ==
"surface") {
903 const auto t1 = std::chrono::steady_clock::now();
904 rep.loadSeconds = std::chrono::duration<double>(
t1 -
t0).count();
907 solid->CloseShape(
true);
908 rep.closeSeconds = std::chrono::duration<double>(std::chrono::steady_clock::now() -
t1).count();
911 rep.surfaceSolid = solid;
912 rep.structural.primitives = solid->GetNsurfaces();
918 rep.structural.bytes = rep.structural.sidecarBytes;
919 rep.structural.formula =
"patches=" +
std::to_string(rep.structural.primitives) +
920 "; bytes = sidecar on disk (in-memory trim arrays are not "
921 "introspectable; see measured heap delta)";
922 }
else if (
name ==
"mesh") {
927 const auto t1 = std::chrono::steady_clock::now();
928 rep.loadSeconds = std::chrono::duration<double>(
t1 -
t0).count();
932 rep.closeSeconds = std::chrono::duration<double>(std::chrono::steady_clock::now() -
t1).count();
935 rep.meshClosedBody = solid->IsClosedBody();
936 rep.structural.primitives = solid->GetNfacets();
939 const long long nF = solid->GetNfacets();
940 const long long nV = solid->GetNvertices();
942 nF *
static_cast<long long>(
sizeof(TGeoFacet)) +
944 rep.structural.formula =
948 }
else if (
name ==
"flatcsg") {
949 auto* solid =
new O2FlatCSG(partId.c_str());
953 if (flatSplitDepth >= 0) {
954 solid->SetSplitDepth(flatSplitDepth);
956 if (flatMinBoxFraction >= 0.) {
957 solid->SetMinBoxFraction(flatMinBoxFraction);
959 const auto t1 = std::chrono::steady_clock::now();
960 rep.loadSeconds = std::chrono::duration<double>(
t1 -
t0).count();
964 rep.closeSeconds = std::chrono::duration<double>(std::chrono::steady_clock::now() -
t1).count();
966 if (!solid->IsClosed()) {
973 rep.flatSolid = solid;
974 rep.structural.primitives = solid->GetNcells();
977 const long long nH = solid->GetNhalfspaces();
978 const long long nC = solid->GetNcells();
979 const long long nB = solid->GetNboxes();
981 for (
int i = 0;
i < solid->GetNboxes(); ++
i) {
982 active += solid->GetBox(
i).nActive;
984 const long long bvh =
static_cast<long long>(solid->GetBVHMemory());
985 rep.structural.bytes = nH *
static_cast<long long>(
sizeof(
FlatCSGHalfspace)) +
987 nC * 6 *
static_cast<long long>(
sizeof(double)) +
988 nB *
static_cast<long long>(
sizeof(
FlatCSGBox)) +
990 rep.structural.formula =
999 if (rep.shape ==
nullptr) {
1003 rep.loadSeconds = std::chrono::duration<double>(std::chrono::steady_clock::now() -
t0).count();
1006 rep.structural.primitives =
tree.leaves;
1009 rep.structural.bytes =
tree.leaves * 200 +
tree.nodes * 200;
1013 "; bytes ~ (leaves+nodes) x 200 B (ROOT object overhead dominates)";
1026 if (placement ==
nullptr) {
1030 for (
auto& p : out.
points) {
1032 placement->MasterToLocal(p.data(), q.data());
1035 auto move = [&](std::vector<Ray>& rays) {
1036 for (
auto&
r : rays) {
1039 placement->MasterToLocal(
r.origin.data(), o.data());
1040 placement->MasterToLocalVect(
r.dir.data(), d.data());
1061 const auto* shape =
static_cast<const TGeoShape*
>(solid);
1068 for (
const auto& ray : s.outsideRays) {
1069 volatile double sink = solid->
DistFromOutside(ray.origin.data(), ray.dir.data(), 3, TGeoShape::Big(),
nullptr);
1073 const TimingStat pruned =
timePasses(
static_cast<long long>(s.outsideRays.size()), warmup, passes, [&]() {
1075 for (const auto& ray : s.outsideRays) {
1076 acc ^= static_cast<uint64_t>(
1077 solid->DistFromOutside(ray.origin.data(), ray.dir.data(), 3, TGeoShape::Big(), nullptr) * 1.e6);
1084 for (
const auto& ray : s.outsideRays) {
1085 volatile double sink = solid->
DistFromOutside(ray.origin.data(), ray.dir.data(), 3, TGeoShape::Big(),
nullptr);
1089 const TimingStat unpruned =
timePasses(
static_cast<long long>(s.outsideRays.size()), warmup, passes, [&]() {
1091 for (const auto& ray : s.outsideRays) {
1092 acc ^= static_cast<uint64_t>(
1093 solid->DistFromOutside(ray.origin.data(), ray.dir.data(), 3, TGeoShape::Big(), nullptr) * 1.e6);
1099 const TimingStat loop =
timePasses(
static_cast<long long>(
s.outsideRays.size()), warmup, passes, [&]() {
1101 for (const auto& ray : s.outsideRays) {
1102 acc ^= static_cast<uint64_t>(
1103 solid->DistFromOutside_Loop(ray.origin.data(), ray.dir.data()) * 1.e6);
1107 const TimingStat containsLoop =
timePasses(
static_cast<long long>(
s.points.size()), warmup, passes, [&]() {
1109 for (const auto& p : s.points) {
1110 acc ^= solid->Contains_Loop(p.data()) ? 1u : 0u;
1123 long long safetyDisagreements = 0;
1124 long long normalDisagreements = 0;
1126 const bool inside =
s.pointIsInside[
index] != 0;
1127 if (solid->Safety(
s.points[
index].data(), inside) != solid->Safety_Loop(
s.points[
index].data(), inside)) {
1128 ++safetyDisagreements;
1132 solid->ComputeNormal(
s.points[
index].data(),
nullptr, viaBVH.data());
1133 solid->ComputeNormal_Loop(
s.points[
index].data(),
nullptr, viaLoop.data());
1134 if (viaBVH != viaLoop) {
1135 ++normalDisagreements;
1140 volatile double sink = solid->Safety(
s.points[
index].data(),
s.pointIsInside[
index] != 0);
1145 const TimingStat safetyBVH =
timePasses(
static_cast<long long>(
s.points.size()), warmup, passes, [&]() {
1147 for (size_t index = 0; index < s.points.size(); ++index) {
1148 acc ^= static_cast<uint64_t>(solid->Safety(s.points[index].data(), s.pointIsInside[index] != 0) * 1.e6);
1152 const TimingStat safetyLoop =
timePasses(
static_cast<long long>(
s.points.size()), warmup, passes, [&]() {
1154 for (size_t index = 0; index < s.points.size(); ++index) {
1155 acc ^= static_cast<uint64_t>(solid->Safety_Loop(s.points[index].data(), s.pointIsInside[index] != 0) * 1.e6);
1159 const TimingStat normalBVH =
timePasses(
static_cast<long long>(
s.points.size()), warmup, passes, [&]() {
1161 Point3D normal{0., 0., 0.};
1162 for (
const auto& p :
s.points) {
1163 solid->ComputeNormal(
p.data(),
nullptr, normal.data());
1164 acc ^=
static_cast<uint64_t
>(normal[0] * 1.e6);
1168 const TimingStat normalLoop =
timePasses(
static_cast<long long>(
s.points.size()), warmup, passes, [&]() {
1170 Point3D normal{0., 0., 0.};
1171 for (
const auto& p :
s.points) {
1172 solid->ComputeNormal_Loop(
p.data(),
nullptr, normal.data());
1173 acc ^=
static_cast<uint64_t
>(normal[0] * 1.e6);
1178 const double rays =
static_cast<double>(std::max<size_t>(1,
s.outsideRays.size()));
1179 const double points =
static_cast<double>(std::max<size_t>(1,
s.points.size()));
1181 out[
"safetyLoopNs"] = safetyLoop.medianNsPerCall;
1183 out[
"normalBVHNs"] = normalBVH.medianNsPerCall;
1184 out[
"normalLoopNs"] = normalLoop.medianNsPerCall;
1185 out[
"bvhCandidatesPerSafetyCall"] = safetyCandidates / points;
1186 out[
"loopCandidatesPerSafetyCall"] =
static_cast<double>(solid->GetNsurfaces());
1187 out[
"safetyDisagreements"] = safetyDisagreements;
1188 out[
"normalDisagreements"] = normalDisagreements;
1189 out[
"nearestPatchComparedPoints"] =
static_cast<long long>(
s.points.size());
1191 " safety: %.1f ns BVH vs %.1f ns _Loop (%.1fx) | %.2f candidates/call of %d | "
1192 "normal %.1f ns vs %.1f ns | disagreements %lld safety / %lld normal in %zu points\n",
1193 safetyBVH.
medianNsPerCall, safetyLoop.medianNsPerCall, out[
"safetySpeedup"].get<
double>(),
1194 safetyCandidates / points, solid->GetNsurfaces(), normalBVH.medianNsPerCall,
1195 normalLoop.medianNsPerCall, safetyDisagreements, normalDisagreements,
s.points.size());
1197 out[
"patches"] = solid->GetNsurfaces();
1198 out[
"bvhCandidatesPerDistOutCall"] = prunedCandidates / rays;
1199 out[
"loopCandidatesPerDistOutCall"] = unprunedCandidates / rays;
1200 out[
"distOutPrunedNs"] = pruned.medianNsPerCall;
1201 out[
"distOutUnprunedNs"] = unpruned.medianNsPerCall;
1203 out[
"containsLoopNs"] = containsLoop.medianNsPerCall;
1204 out[
"nsPerBVHCandidate"] =
1205 prunedCandidates > 0 ? pruned.medianNsPerCall * rays /
static_cast<double>(prunedCandidates) : 0.;
1207 " localise: %d patches | %.1f BVH candidates/distout call (unpruned %.1f) | "
1208 "distout %.1f ns pruned, %.1f ns unpruned, %.1f ns _Loop | %.2f ns per candidate patch | "
1209 "Contains_Loop %.1f ns\n",
1210 solid->GetNsurfaces(), prunedCandidates / rays, unprunedCandidates / rays,
1211 pruned.medianNsPerCall, unpruned.medianNsPerCall, loop.
medianNsPerCall,
1212 out[
"nsPerBVHCandidate"].get<
double>(), containsLoop.medianNsPerCall);
1218 std::printf(
" %-14s %9.1f ns/call [%9.1f .. %9.1f, spread %5.1f%%]",
label,
1221 std::printf(
" hit %5.1f%%", 100. * t.
hitFraction);
1233 json out = json::array();
1234 std::vector<int> counts;
1236 std::stringstream ss(opt.ladderSpec);
1238 while (std::getline(ss, tok,
',')) {
1240 counts.push_back(std::stoi(tok));
1244 std::printf(
"=== synthetic boolean ladder: unions of K overlapping TGeoTubes ===\n");
1246 " Every genuine boolean in the corpus is a 2-leaf union of two TGeoTubes, so the\n"
1247 " corpus cannot say how a composite scales with K. This can.\n\n");
1248 for (
const int k : counts) {
1249 for (
const auto shapeKind : {LadderShape::Chain, LadderShape::Balanced}) {
1250 const char* kindName = shapeKind == LadderShape::Chain ?
"chain" :
"balanced";
1251 auto* manager =
new TGeoManager(
"ladder",
"boolean ladder");
1252 const std::string tag = std::string(
"L") + kindName +
std::to_string(k);
1256 if (shape ==
nullptr) {
1258 gGeoManager =
nullptr;
1262 const auto*
box =
dynamic_cast<const TGeoBBox*
>(shape);
1264 box->GetOrigin()[2] -
box->GetDZ()};
1266 box->GetOrigin()[2] +
box->GetDZ()};
1273 std::printf(
" --- K=%-3d %-9s (leaves=%lld nodes=%lld depth=%d, %.1f%% of points inside) ---\n",
1275 100. *
static_cast<double>(
samples.insidePoints) /
1276 static_cast<double>(std::max<size_t>(1,
samples.points.size())));
1277 printTiming(
"Contains", contains);
1278 printTiming(
"Safety", safety);
1279 printTiming(
"DistFromOutside", distOut);
1280 printTiming(
"DistFromInside", distIn);
1281 out.push_back({{
"leavesRequested", k},
1282 {
"treeShape", kindName},
1283 {
"leaves",
tree.leaves},
1284 {
"nodes",
tree.nodes},
1285 {
"depth",
tree.depth},
1286 {
"buildResidentBytes", (after - before).residentBytes},
1287 {
"buildHeapBytes", (after - before).heapInUseBytes},
1288 {
"insideFraction",
static_cast<double>(
samples.insidePoints) /
1289 static_cast<double>(std::max<size_t>(1,
samples.points.size()))},
1290 {
"contains", timingToJson(contains)},
1291 {
"safety", timingToJson(safety)},
1292 {
"distFromOutside", timingToJson(distOut)},
1293 {
"distFromInside", timingToJson(distIn)}});
1295 gGeoManager =
nullptr;
1308 auto check = [&](
const char*
name,
bool ok,
const std::string& detail = {}) {
1309 std::printf(
" [%s] %s%s\n", ok ?
"ok " :
"FAIL",
name,
1310 ok || detail.
empty() ?
"" : (
" " + detail).c_str());
1323 const Point3D dir{1., 0., 0.};
1325 check(
"box: exactly two crossings along a central ray", crossings.size() == 2,
1327 if (crossings.size() == 2) {
1328 check(
"box: enter at 4.0 cm", std::fabs(crossings[0].t - 4.) < 1.e-9);
1329 check(
"box: exit at 6.0 cm", std::fabs(crossings[1].t - 6.) < 1.e-9);
1330 check(
"box: kinds are enter then exit", crossings[0].
kind == +1 && crossings[1].
kind == -1);
1337 TGeoTube tube(
"selftestTube", 0.5, 1.0, 2.0);
1339 const Point3D dir{1., 0., 0.};
1341 check(
"hollow tube: four crossings along a diameter", crossings.size() == 4,
1343 if (crossings.size() == 4) {
1344 const double expect[4] = {4.0, 4.5, 5.5, 6.0};
1346 for (
int i = 0;
i < 4; ++
i) {
1347 ok = ok && std::fabs(crossings[
i].t - expect[
i]) < 1.e-9;
1349 check(
"hollow tube: crossings at 4.0 / 4.5 / 5.5 / 6.0 cm", ok);
1350 check(
"hollow tube: in, out, in, out",
1351 crossings[0].
kind == +1 && crossings[1].
kind == -1 && crossings[2].
kind == +1 &&
1352 crossings[3].
kind == -1);
1363 const Point3D bboxMin{-1., -1.5, -2.};
1364 const Point3D bboxMax{1., 1.5, 2.};
1365 for (
const int n : {7, 32}) {
1368 std::vector<double> byAxis(3, 0.);
1369 for (
const auto& ray : raster.rays) {
1370 const double before =
s.insideLength;
1373 byAxis[ray.beam] +=
s.insideLength - before;
1375 const double volume =
chordVolume(raster, byAxis);
1377 std::fabs(volume - 24.) < 1.e-9,
"got " +
std::to_string(volume));
1387 TGeoSphere sphere(
"selftestSphere", 0., 1.);
1388 const Point3D bboxMin{-1., -1., -1.};
1389 const Point3D bboxMax{1., 1., 1.};
1390 const double exact = 4. / 3. * 3.14159265358979323846;
1392 for (
const int n : {24, 48, 96, 192}) {
1395 for (
const auto& ray : raster.rays) {
1396 auto crossings =
stepWithShapeApi(&sphere, ray.origin, ray.dir, ray.tMax, cfg, s);
1399 const double volume =
s.insideLength * raster.
cellArea[0];
1400 const double rel = std::fabs(volume - exact) / exact;
1401 worst = std::max(worst, rel);
1403 " sphere r=1: raster %3d x %3d -> V = %.8f cm^3, exact %.8f, "
1405 n,
n, volume, exact, rel);
1410 check(
"sphere chord integral stays inside the measured 2e-3 envelope for N = 24..192",
1418 const std::vector<Crossing> truth{{4.0, +1}, {4.5, -1}, {5.5, +1}, {6.0, -1}};
1424 check(
"control 0: identical lists compare clean",
1428 auto perturbed = truth;
1429 perturbed[2].t += 1.e-3;
1432 check(
"control 1: a crossing moved by 1e-3 cm is CAUGHT, and as DISPLACED not as lost",
1438 auto dropped = truth;
1439 dropped.erase(dropped.begin() + 1);
1442 check(
"control 2: a dropped crossing is CAUGHT as `missing`",
1445 auto doubled = truth;
1446 doubled.insert(doubled.begin() + 1, {4.2, -1});
1449 check(
"control 3: an extra crossing is CAUGHT as `extra`",
1454 auto flipped = truth;
1455 flipped[1].kind = +1;
1458 check(
"control 4: a crossing with the wrong sense is CAUGHT", sense.
kindMismatch == 1);
1466 TGeoBBox fast(
"perfControlFast", 1., 1., 1.);
1468 const Point3D lo{-1., -1., -1.};
1471 check(
"control 5: the shared sample set has both inside and outside points",
1473 samples.insidePoints <
static_cast<long long>(
samples.points.size()) - 100,
1476 check(
"control 6: the sample partition is consistent with the reference it came from", [&] {
1477 for (
size_t i = 0;
i <
samples.points.size(); ++
i) {
1478 if (fast.Contains(
samples.points[
i].data()) != (
samples.pointIsInside[
i] != 0)) {
1484 check(
"control 7: DistFromOutside rays actually hit (an all-miss set times the early-out)",
1495 for (
const auto& kernel : kernels) {
1499 check((std::string(
"control 8: ballast is VISIBLE on ") + kernel.name +
1500 " (the timing harness can move its own number)")
1505 check((std::string(
"control 9: the ") + kernel.name +
1506 " timing loop was not elided (non-zero checksum, positive time)")
1517 constexpr size_t kBytes = 64u << 20;
1518 auto* block =
new char[kBytes];
1519 for (
size_t i = 0;
i < kBytes;
i += 4096) {
1520 block[
i] =
static_cast<char>(
i);
1524 check(
"control 10: the resident probe sees a 64 MB touched allocation",
1527 check(
"control 11: the heap probe sees a 64 MB allocation",
1532 check(
"control 12: the heap probe sees it released again (the resident one need not)",
1542 auto* manager =
new TGeoManager(
"perfControlLadder",
"structural control");
1547 check(
"control 13: the ladder builds the leaf count it was asked for",
1548 a.leaves == 4 &&
b.leaves == 32,
1550 check(
"control 14: a balanced ladder's depth is logarithmic in its leaf count",
1551 a.depth == 3 &&
b.depth == 6,
1556 "control 15: a chain ladder of the same leaf count is deeper, so the two tree shapes "
1557 "really are different fixtures",
1558 c.leaves == 32 &&
c.depth == 32,
1561 gGeoManager =
nullptr;
1569 const Point3D dir{1., 0., 0.};
1575 check(
"parity audit: a truncated list is CAUGHT by Contains() at the midpoints",
1579 std::printf(
"\n%s: %d failure(s)\n", failures == 0 ?
"SELF-TEST PASSED" :
"SELF-TEST FAILED",
1581 return failures == 0 ? 0 : 1;
1590 if (!parseArgs(argc, argv, opt)) {
1593 }
catch (
const std::exception& e) {
1594 std::cerr <<
"error: " << e.what() <<
"\n";
1595 printUsage(argv[0]);
1603 if (!opt.ladderSpec.empty()) {
1604 json ladder = runLadder(opt);
1605 if (!opt.jsonOut.empty()) {
1606 std::ofstream out(opt.jsonOut);
1607 out <<
json{{
"ladder", std::move(ladder)}}.dump(1);
1608 std::printf(
"\nreport: %s\n", opt.jsonOut.c_str());
1613 std::vector<Beam> beams;
1614 std::vector<Part> parts;
1618 if (beams.empty()) {
1619 throw std::runtime_error(
"no beam selected (--axes)");
1621 parts = collectParts(opt);
1622 }
catch (
const std::exception& e) {
1623 std::cerr <<
"error: " << e.what() <<
"\n";
1626 if (parts.empty()) {
1627 std::cerr <<
"no parts matched (pattern='" << opt.partsPattern <<
"')\n";
1636 "Per-call costs are WARM-CACHE, single-threaded, median of %d passes after %d "
1637 "warmup passes.\nEvery representation of a part answers the SAME sample set.\n\n",
1638 opt.perfPasses, opt.perfWarmup);
1639 for (
const auto& part : parts) {
1640 std::printf(
"=== %s (%s) ===\n", part.id.c_str(), part.model.c_str());
1643 std::string bboxSource;
1644 if (!resolveBoundingBox(part, opt, lo, hi, bboxSource)) {
1645 std::printf(
" skip: no representation could supply a bounding box\n");
1656 std::string partitionedBy;
1657 for (
const auto& candidate : allRepresentations()) {
1658 const std::string&
source = sourceFor(part, candidate);
1662 LoadedRep rep = loadRepresentation(candidate,
source, part.id, opt.flatSplitDepth,
1663 opt.flatMinBoxFraction);
1668 if (rep.placement) {
1672 for (
auto& p : inFrame.
points) {
1674 rep.placement->LocalToMaster(p.data(), q.data());
1678 for (
auto&
r : *rays) {
1681 rep.placement->LocalToMaster(
r.origin.data(), o.data());
1682 rep.placement->LocalToMasterVect(
r.dir.data(), d.data());
1689 partitionedBy = candidate;
1692 gGeoManager =
nullptr;
1693 if (!partitionedBy.empty()) {
1697 if (partitionedBy.empty()) {
1698 std::printf(
" skip: no representation loaded\n");
1701 samples.partitionedBy = partitionedBy;
1703 " samples: %zu points (%.1f%% inside), %zu outside rays, %zu inside rays, "
1704 "partitioned by '%s'; raster %d x %d x %zu beams = %zu rays\n",
1706 100. *
static_cast<double>(
samples.insidePoints) /
1707 static_cast<double>(std::max<size_t>(1,
samples.points.size())),
1708 samples.outsideRays.size(),
samples.insideRays.size(), partitionedBy.c_str(),
1709 raster.
n, raster.
n, raster.
beams.size(), raster.
rays.size());
1712 partJson[
"id"] = part.id;
1713 partJson[
"model"] = part.model;
1714 partJson[
"partitionedBy"] = partitionedBy;
1715 partJson[
"insideFraction"] =
static_cast<double>(
samples.insidePoints) /
1716 static_cast<double>(std::max<size_t>(1,
samples.points.size()));
1717 partJson[
"bboxSource"] = bboxSource;
1718 json repsJson = json::array();
1720 for (
const auto& candidate : allRepresentations()) {
1721 const std::string&
source = sourceFor(part, candidate);
1725 LoadedRep rep = loadRepresentation(candidate,
source, part.id, opt.flatSplitDepth,
1726 opt.flatMinBoxFraction);
1728 std::printf(
" [skip %s] would not load from %s\n", candidate.c_str(),
source.c_str());
1730 gGeoManager =
nullptr;
1734 std::printf(
" --- %-8s %-22s (%lld %s, load %.3f s + close %.3f s) ---\n", candidate.c_str(),
1735 rep.shape->ClassName(), rep.structural.primitives,
1736 candidate ==
"mesh" ?
"triangles"
1737 : candidate ==
"surface" ?
"patches"
1738 : candidate ==
"flatcsg" ?
"cells"
1740 rep.loadSeconds, rep.closeSeconds);
1746 printTiming(
"Contains", contains);
1747 printTiming(
"Safety", safety);
1748 printTiming(
"DistFromOutside", distOut);
1749 printTiming(
"DistFromInside", distIn);
1755 long long crossings = 0;
1757 timePasses(
static_cast<long long>(raster.
rays.size()), opt.perfWarmup, opt.perfPasses, [&]() {
1760 long long found = 0;
1761 for (const auto& ray : raster.rays) {
1764 toShapeFrame(rep.placement.get(), ray.origin, ray.dir, o, d);
1765 const auto list = stepWithShapeApi(rep.shape, o, d, ray.tMax, opt.step, s);
1766 found += static_cast<long long>(list.size());
1779 const double nsPerCrossing =
1781 static_cast<double>(crossings)
1784 " %-14s %9.1f ns/ray [%9.1f .. %9.1f, spread %5.1f%%] %.1f ns/crossing "
1785 "(%lld crossings, %lld steps)\n",
1786 "transport", transport.medianNsPerCall, transport.minNsPerCall,
1787 transport.maxNsPerCall, 100. * transport.spread, nsPerCrossing, crossings,
1788 statsTransport.steps);
1791 rep.loadDelta.heapInUseBytes + rep.closeDelta.heapInUseBytes};
1793 " memory: structural %lld B (%s)\n"
1794 " sidecar on disk %lld B | measured heap +%lld B (load %lld + close "
1795 "%lld) | resident +%lld B\n",
1796 rep.structural.bytes, rep.structural.formula.c_str(),
1797 rep.structural.sidecarBytes, total.heapInUseBytes, rep.loadDelta.heapInUseBytes,
1798 rep.closeDelta.heapInUseBytes, total.residentBytes);
1799 if (candidate ==
"mesh" && !rep.meshClosedBody) {
1801 " *** meshClosedBody = FALSE: this mesh is INVALID, not merely "
1802 "inaccurate. Read no accuracy column of this row as a safety statement. ***\n");
1806 repJson[
"name"] = candidate;
1807 repJson[
"source"] =
source;
1808 repJson[
"shapeClass"] = rep.shape->ClassName();
1809 repJson[
"primitives"] = rep.structural.primitives;
1810 repJson[
"loadSeconds"] = rep.loadSeconds;
1811 repJson[
"closeSeconds"] = rep.closeSeconds;
1812 repJson[
"structuralBytes"] = rep.structural.bytes;
1813 repJson[
"structuralFormula"] = rep.structural.formula;
1814 repJson[
"sidecarBytes"] = rep.structural.sidecarBytes;
1815 repJson[
"heapBytesLoad"] = rep.loadDelta.heapInUseBytes;
1816 repJson[
"heapBytesClose"] = rep.closeDelta.heapInUseBytes;
1817 repJson[
"heapBytesTotal"] = total.heapInUseBytes;
1818 repJson[
"residentBytesTotal"] = total.residentBytes;
1819 repJson[
"capacity"] = rep.shape->Capacity();
1820 repJson[
"placed"] = (rep.placement !=
nullptr);
1821 repJson[
"contains"] = timingToJson(contains);
1822 repJson[
"safety"] = timingToJson(safety);
1823 repJson[
"distFromOutside"] = timingToJson(distOut);
1824 repJson[
"distFromInside"] = timingToJson(distIn);
1825 repJson[
"transport"] = timingToJson(transport);
1826 repJson[
"transportNsPerCrossing"] = nsPerCrossing;
1827 repJson[
"transportCrossings"] = crossings;
1828 repJson[
"transportSteps"] = statsTransport.steps;
1829 repJson[
"transportUnterminated"] = statsTransport.unterminated;
1830 repJson[
"transportParityMismatch"] = statsTransport.parityMismatchIntervals;
1831 if (candidate ==
"mesh") {
1832 repJson[
"meshClosedBody"] = rep.meshClosedBody;
1834 if (rep.surfaceSolid !=
nullptr) {
1835 repJson[
"localise"] = localiseSurfaceSolid(rep.surfaceSolid, local, opt.perfWarmup,
1838 if (rep.flatSolid !=
nullptr) {
1842 long long worst = 0;
1843 for (
int i = 0;
i < rep.flatSolid->GetNboxes(); ++
i) {
1844 const long long n = rep.flatSolid->GetBox(
i).nActive;
1846 worst = std::max(worst,
n);
1848 repJson[
"flatCells"] = rep.flatSolid->GetNcells();
1849 repJson[
"flatHalfspaces"] = rep.flatSolid->GetNhalfspaces();
1850 repJson[
"flatBoxes"] = rep.flatSolid->GetNboxes();
1851 repJson[
"flatActiveTotal"] =
active;
1852 repJson[
"flatActiveMean"] =
1853 rep.flatSolid->GetNboxes() > 0
1854 ?
static_cast<double>(
active) /
static_cast<double>(rep.flatSolid->GetNboxes())
1856 repJson[
"flatActiveMax"] = worst;
1857 repJson[
"flatBVHBytes"] =
static_cast<long long>(rep.flatSolid->GetBVHMemory());
1858 repJson[
"flatSplitDepth"] = opt.flatSplitDepth;
1859 repJson[
"flatMinBoxFraction"] = opt.flatMinBoxFraction;
1860 repJson[
"flatCloseSeconds"] = rep.closeSeconds;
1862 repsJson.push_back(std::move(repJson));
1864 gGeoManager =
nullptr;
1866 partJson[
"representations"] = std::move(repsJson);
1867 report.push_back(std::move(partJson));
1870 if (!opt.jsonOut.empty()) {
1871 std::ofstream out(opt.jsonOut);
1873 std::printf(
"\nreport: %s\n", opt.jsonOut.c_str());
1878 for (
const auto& part : parts) {
1879 std::printf(
"=== %s (%s) ===\n", part.id.c_str(), part.model.c_str());
1881 partJson[
"id"] = part.id;
1882 partJson[
"model"] = part.model;
1889 OracleCrossings oracle;
1890 std::string bboxSource =
"?";
1891 if (!opt.refCrossings.empty()) {
1893 oracle = loadOracleCrossings(opt.refCrossings, part.id);
1894 }
catch (
const std::exception& e) {
1895 std::cerr <<
" error reading crossings: " << e.what() <<
"\n";
1899 std::printf(
" skip: no crossings file for this part in %s\n", opt.refCrossings.c_str());
1902 raster = oracle.raster;
1903 opt.step.matchTolerance = std::max(oracle.tolerance, 1.e-6);
1905 " oracle: %s tolerance=%.3g capacity=%.6g cm^3 chordVolume=%.6g cm^3 "
1906 "(%lld ambiguous ray(s))\n",
1907 oracle.valid ?
"valid" :
"*** NOT BRepCheck-VALID ***", oracle.tolerance,
1908 oracle.capacity, oracle.volumeChord, oracle.ambiguousRays);
1912 if (!resolveBoundingBox(part, opt, lo, hi, bboxSource)) {
1913 std::printf(
" skip: no representation could supply a bounding box\n");
1916 raster =
buildRaster(lo, hi, opt.raster, beams, opt.margin);
1918 " raster: %d x %d x %zu beam(s) = %zu rays (tilt %.3g deg); window from the "
1919 "'%s' bounding box + %.3g cm, cross-section excess %.3g\n",
1920 raster.
n, raster.
n, raster.
beams.size(), raster.
rays.size(), opt.tiltDegrees,
1926 if (!opt.dumpRays.empty()) {
1927 writeRays(opt.dumpRays, part.id, raster, bboxSource);
1936 std::vector<RepSpec> specs;
1937 if (opt.representations.count(
"surface") &&
fileExists(part.surfaces)) {
1938 specs.push_back({
"surface", part.surfaces});
1940 if (opt.representations.count(
"mesh") &&
fileExists(part.facets)) {
1941 specs.push_back({
"mesh", part.facets});
1943 if (opt.representations.count(
"shape") &&
fileExists(part.shape)) {
1944 specs.push_back({
"shape", part.shape});
1946 if (opt.representations.count(
"flatcsg") &&
fileExists(part.flatcsg)) {
1947 specs.push_back({
"flatcsg", part.flatcsg});
1949 if (specs.empty()) {
1950 std::printf(
" skip: no representation available\n");
1954 json repsJson = json::array();
1956 for (
const auto& spec : specs) {
1960 auto* manager =
new TGeoManager((
"xray_" + spec.name).c_str(),
"X-ray benchmark world");
1961 TGeoShape* shape =
nullptr;
1964 std::unique_ptr<TGeoHMatrix> placement;
1965 double loadSeconds = 0.;
1966 int primitives = -1;
1967 const char* primitiveKind =
"";
1968 const auto tLoad0 = std::chrono::steady_clock::now();
1969 if (spec.name ==
"surface") {
1972 std::printf(
" [skip %s] LoadSurfaceSolid failed for %s\n", spec.name.c_str(),
1973 spec.source.c_str());
1975 gGeoManager =
nullptr;
1978 solid->CloseShape(
true);
1979 primitives = solid->GetNsurfaces();
1980 primitiveKind =
"patches";
1982 }
else if (spec.name ==
"mesh") {
1985 std::printf(
" [skip %s] LoadFacetSolid failed for %s\n", spec.name.c_str(),
1986 spec.source.c_str());
1988 gGeoManager =
nullptr;
1991 solid->CloseShape();
1992 primitives = solid->GetNfacets();
1993 primitiveKind =
"triangles";
1995 }
else if (spec.name ==
"flatcsg") {
1996 auto* solid =
new O2FlatCSG(part.id.c_str());
1997 if (opt.flatSplitDepth >= 0) {
1998 solid->SetSplitDepth(opt.flatSplitDepth);
2000 if (opt.flatMinBoxFraction >= 0.) {
2001 solid->SetMinBoxFraction(opt.flatMinBoxFraction);
2004 std::printf(
" [skip %s] LoadFlatCSG failed for %s\n", spec.name.c_str(),
2005 spec.source.c_str());
2007 gGeoManager =
nullptr;
2010 solid->CloseShape();
2011 if (!solid->IsClosed()) {
2013 " [skip %s] CloseShape refused %s, so the shape would answer through its "
2014 "_Loop twins and the row would not be the accelerated path\n",
2015 spec.name.c_str(), spec.source.c_str());
2017 gGeoManager =
nullptr;
2020 primitives = solid->GetNcells();
2021 primitiveKind =
"cells";
2026 if (shape ==
nullptr) {
2027 std::printf(
" [skip %s] %s\n", spec.name.c_str(),
error.c_str());
2029 gGeoManager =
nullptr;
2033 primitiveKind = shape->ClassName();
2035 loadSeconds = std::chrono::duration<double>(std::chrono::steady_clock::now() - tLoad0).count();
2037 const auto*
box =
dynamic_cast<const TGeoBBox*
>(shape);
2039 std::printf(
" --- %-8s %-28s (%d %s, load %.3f s) ---\n", spec.name.c_str(),
2040 shape->ClassName(), primitives, primitiveKind, loadSeconds);
2043 repJson[
"name"] = spec.name;
2044 repJson[
"source"] = spec.source;
2045 repJson[
"shapeClass"] = shape->ClassName();
2046 repJson[
"primitives"] = primitives;
2047 repJson[
"primitiveKind"] = primitiveKind;
2048 repJson[
"loadSeconds"] = loadSeconds;
2049 repJson[
"capacity"] = shape->Capacity();
2050 repJson[
"placed"] = (placement !=
nullptr);
2054 std::vector<double> insideByAxisA(raster.
beams.size(), 0.);
2055 std::vector<std::vector<Crossing>> listsA(raster.
rays.size());
2058 const auto t0 = std::chrono::steady_clock::now();
2059 for (
size_t i = 0;
i < raster.
rays.size(); ++
i) {
2060 const auto& ray = raster.
rays[
i];
2064 toShapeFrame(placement.get(), ray.origin, ray.dir, o, d);
2067 insideByAxisA[ray.beam] += statsA.
insideLength - before;
2069 statsA.
seconds = std::chrono::duration<double>(std::chrono::steady_clock::now() -
t0).count();
2071 repJson[
"modeA"] = robustnessToJson(statsA);
2072 repJson[
"modeA"][
"volumeChordCm3"] =
chordVolume(raster, insideByAxisA);
2073 json perAxisA = json::object();
2074 for (
size_t b = 0;
b < raster.
beams.size(); ++
b) {
2077 repJson[
"modeA"][
"volumeChordPerAxisCm3"] = perAxisA;
2079 for (
size_t i = 0;
i < raster.
rays.size() &&
i < oracle.perRay.size(); ++
i) {
2080 if (oracle.ambiguous[
i]) {
2084 opt.step.matchTolerance, vsOracleA);
2086 repJson[
"modeA"][
"vsOracle"] = comparisonToJson(vsOracleA);
2089 " (a) shape API : %lld rays, %lld crossings, %.4f s | zero=%lld stall=%lld "
2090 "nonAdv=%lld cap=%lld unterm=%lld odd=%lld dup=%lld parity=%lld\n",
2097 " vs OCCT : %lld/%lld rays identical, LOST=%lld extra=%lld "
2098 "displaced=%lld kind=%lld worst dt=%.3g cm\n",
2102 std::printf(
" worst : %s at o=(%.6g, %.6g, %.6g) d=(%.4g, %.4g, %.4g)\n",
2108 std::printf(
" volume : chord integral %.8g cm^3 (Capacity %.8g)\n",
2109 repJson[
"modeA"][
"volumeChordCm3"].get<double>(), shape->Capacity());
2112 if (!opt.skipNavigator &&
box !=
nullptr) {
2114 std::vector<double> insideByAxisB(raster.
beams.size(), 0.);
2123 placedBox(*
box, placement.get(), wMin, wMax);
2124 for (
const auto& ray : raster.rays) {
2125 for (
int k = 0; k < 3; ++k) {
2126 const double end = ray.origin[k] + ray.tMax * ray.dir[k];
2127 wMin[k] = std::min({wMin[k], ray.origin[k],
end});
2128 wMax[k] = std::max({wMax[k], ray.origin[k],
end});
2131 NavigatorTransport transport(manager, shape, wMin, wMax, placement.get());
2132 const auto t0 = std::chrono::steady_clock::now();
2133 for (
size_t i = 0;
i < raster.
rays.size(); ++
i) {
2134 const auto& ray = raster.
rays[
i];
2136 auto listB = transport.transport(ray.origin, ray.dir, ray.tMax, opt.step, statsB);
2142 toShapeFrame(placement.get(), ray.origin, ray.dir, o, d);
2144 insideByAxisB[ray.beam] += statsB.
insideLength - before;
2145 if (oracle.has &&
i < oracle.perRay.size() && !oracle.ambiguous[
i]) {
2146 compareLists(listB, oracle.perRay[
i], ray.origin, ray.dir, opt.step.matchTolerance,
2149 compareLists(listB, listsA[
i], ray.origin, ray.dir, opt.step.matchTolerance, aVsB);
2151 statsB.
seconds = std::chrono::duration<double>(std::chrono::steady_clock::now() -
t0).count();
2152 repJson[
"modeB"] = robustnessToJson(statsB);
2153 repJson[
"modeB"][
"volumeChordCm3"] =
chordVolume(raster, insideByAxisB);
2155 repJson[
"modeB"][
"vsOracle"] = comparisonToJson(vsOracleB);
2157 repJson[
"modeAvsB"] = comparisonToJson(aVsB);
2159 " (b) navigator: %lld rays, %lld crossings, %.4f s | zero=%lld nonAdv=%lld "
2160 "cap=%lld unterm=%lld odd=%lld dup=%lld noTransition=%lld outsideWorld=%lld\n",
2167 " vs OCCT : %lld/%lld rays identical, LOST=%lld extra=%lld "
2168 "displaced=%lld worst dt=%.3g cm\n",
2173 " (a)vs(b): %lld/%lld rays identical, LOST=%lld extra=%lld "
2174 "displaced=%lld worst dt=%.3g cm\n",
2177 std::printf(
" volume : chord integral %.8g cm^3\n",
2178 repJson[
"modeB"][
"volumeChordCm3"].get<double>());
2181 repsJson.push_back(std::move(repJson));
2183 gGeoManager =
nullptr;
2186 partJson[
"raster"] = {{
"n", raster.
n},
2187 {
"rays", raster.
rays.size()},
2199 partJson[
"oracle"] = {{
"tolerance", oracle.tolerance},
2200 {
"capacity", oracle.capacity},
2201 {
"volumeChordCm3", oracle.volumeChord},
2202 {
"chordVsExactRelative",
2203 oracle.capacity != 0.
2204 ? (oracle.volumeChord - oracle.capacity) / oracle.capacity
2206 {
"ambiguousRays", oracle.ambiguousRays},
2207 {
"valid", oracle.valid}};
2209 " raster precision: OCCT chord integral %.8g vs OCCT exact %.8g "
2210 "-> %.3e relative (N=%d, %zu rays)\n",
2211 oracle.volumeChord, oracle.capacity,
2212 partJson[
"oracle"][
"chordVsExactRelative"].get<
double>(), raster.
n,
2213 raster.
rays.size());
2215 partJson[
"representations"] = std::move(repsJson);
2216 report.push_back(std::move(partJson));
2219 if (!opt.jsonOut.empty()) {
2220 std::ofstream out(opt.jsonOut);
2222 std::printf(
"\nreport: %s\n", opt.jsonOut.c_str());
header::DataOrigin origin
GPUTPCCFCheckPadBaseline Kernel
Validation and timing harness for TGeoShape navigation, typed on plain TGeoShape*.
bool fileExists(const char *filename)
Per-call cost, memory and the synthetic boolean ladder: the measuring parts of the representation com...
The X-ray transport benchmark's algorithms: stepping, auditing and comparing ordered crossing lists.
Tessellated::Vertex_t Vertex_t
static void ResetSafetyCandidateCounter()
Per-thread count of surfaces handed to distanceSqToPatch by Safety and ComputeNormal since the last r...
Double_t DistFromOutside(const Double_t *point, const Double_t *dir, Int_t iact=1, Double_t step=TGeoShape::Big(), Double_t *safe=nullptr) const override
static bool GetRayTMaxPruning()
static long long GetSafetyCandidateCount()
static void SetRayTMaxPruning(bool enable)
Ray tmax tightening in the distance queries, on by default; it never changes an answer....
static long long GetRayCandidateCount()
static void ResetRayCandidateCounter()
Per-thread count of surfaces handed to the BVH leaf callback by DistFrom* since the last reset.
GLuint const GLchar * name
GLboolean GLboolean GLboolean b
GLsizei GLsizei GLchar * source
GLuint GLsizei const GLchar * label
typedef void(APIENTRYP PFNGLCULLFACEPROC)(GLenum mode)
GLsizei const GLchar *const * path
GLuint GLfloat GLfloat GLfloat GLfloat GLfloat GLfloat GLfloat t0
GLboolean GLboolean GLboolean GLboolean a
GLsizei const GLint * box
GLuint GLfloat GLfloat GLfloat GLfloat GLfloat GLfloat GLfloat GLfloat GLfloat t1
void report(gsl::span< o2::InteractionTimeRecord > irs, int threshold, bool verbose)
TimingStat timeSafetyPass(const TGeoShape *shape, const QuerySamples &s, int warmup, int passes)
long long fileBytes(const std::string &path)
QuerySamples buildQuerySamples(const TGeoShape *reference, const std::string &referenceName, const Point3D &bboxMin, const Point3D &bboxMax, int nPoints, int nRays, uint64_t seed=20260802ULL, double inflate=0.12)
TGeoShape * buildBooleanLadder(int leaves, LadderShape shape, const std::string &tag)
TimingStat timeContainsPass(const TGeoShape *shape, const QuerySamples &s, int warmup, int passes)
MemorySnapshot readMemory()
TimingStat timePasses(long long callsPerPass, int warmupPasses, int passes, Pass &&pass)
TimingStat timeDistInPass(const TGeoShape *shape, const QuerySamples &s, int warmup, int passes)
TimingStat timeDistOutPass(const TGeoShape *shape, const QuerySamples &s, int warmup, int passes)
BooleanTreeStats booleanTreeStats(const TGeoShape *shape)
TGeoHMatrix * loadShapePlacementFromRootFile(const std::string &path)
Read the shape's placement, or nullptr when there is none, meaning the identity. The caller owns it.
TGeoShape * loadShapeFromRootFile(const std::string &path, std::string *error=nullptr)
Read the single TGeoShape of a shape_<part>.root sidecar; nullptr on failure, with the reason in *err...
std::array< double, 3 > Point3D
std::vector< Crossing > stepWithShapeApi(const TGeoShape *shape, const Point3D &origin, const Point3D &dir, double tMax, const StepConfig &cfg, Robustness &stats)
Mode (a): the same loop driven by the ordinary TGeoShape virtuals.
void compareLists(const std::vector< Crossing > &candidate, const std::vector< Crossing > &reference, const Point3D &origin, const Point3D &dir, double tolerance, ListComparison &out)
double chordVolume(const Raster &raster, const std::vector< double > &insideLengthPerBeam)
void auditCrossingList(const std::vector< Crossing > &crossings, const TGeoShape *shape, const Point3D &origin, const Point3D &dir, double tMax, const StepConfig &cfg, Robustness &stats)
std::vector< Beam > buildFanBeams(int count)
Raster buildRaster(const Point3D &bboxMin, const Point3D &bboxMax, int n, const std::vector< Beam > &beams, double transverseMargin)
std::vector< Beam > buildBeams(const std::string &axesSpec, double tiltDegrees)
bool LoadFlatCSG(const std::string &file, O2FlatCSG &solid)
Load a flat-CSG sidecar (flatcsg_*.bin, version 1) into solid; call CloseShape() after....
bool LoadFacetSolid(const std::string &file, o2::base::O2Tessellated &solid)
bool LoadSurfaceSolid(const std::string &file, O2BVHSurfaceSolid &solid)
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)
One DNF cell: [first, first + count) of the halfspace array, intersected; volume is its own volume.
std::vector< Ray > insideRays
origin inside per the reference, isotropic direction
std::vector< Ray > outsideRays
origin outside per the reference, aimed into the bbox
std::vector< Point3D > points
all query points, mixed inside/outside, in bbox order
long long displaced
same position in both lists, more than tolerance apart
long long raysIdentical
the whole ordered list matched, position and sense
double worstDeltaT
max |dt| over positionally matched crossings, cm
long long missing
in the reference, absent from the candidate
long long extra
in the candidate, absent from the reference
Point3D windowMin
the part bbox plus the margin, in world coordinates (the world box)
std::vector< RayDef > rays
std::vector< double > windowExcess
std::vector< Beam > beams
std::vector< double > cellArea
int beam
index into Raster::beams
long long zeroLengthSteps
a step at or below zeroStep (default 1e-9 cm)
long long unterminated
the ray ended INSIDE the solid: entered and never left
long long duplicateCrossings
long long originOutsideWorld
long long iterationCapHits
the loop hit maxIter without leaving the window
long long unstickPushes
a stalled step that had to be nudged to continue
long long oddCrossingLists
long long parityMismatchIntervals
double insideLength
summed inside-segment length, cm (the chord integral)
long long nonAdvancingSteps
the accumulated distance did not increase
long long boundaryWithoutTransition
double zeroStep
A step at or below this is a stall, not progress.
std::unique_ptr< TTree > tree((TTree *) flIn.Get(std::string(o2::base::NameConf::CTFTREENAME).c_str()))