Project
Loading...
Searching...
No Matches
runXRayBenchmark.cxx
Go to the documentation of this file.
1// Copyright 2019-2026 CERN and copyright holders of ALICE O2.
2// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders.
3// All rights not expressly granted are reserved.
4//
5// This software is distributed under the terms of the GNU General Public
6// License v3 (GPL Version 3), copied verbatim in the file "COPYING".
7//
8// In applying this license CERN does not waive the privileges and immunities
9// granted to it by virtue of its status as an Intergovernmental Organization
10// or submit itself to any jurisdiction.
13
50
51#include "RepresentationBench.h"
52#include "XRayTransport.h"
53
59
60#include "TGeoBBox.h"
61#include "TGeoManager.h"
62#include "TGeoMaterial.h"
63#include "TGeoMatrix.h"
64#include "TGeoMedium.h"
65#include "TGeoNavigator.h"
66#include "TGeoNode.h"
67#include "TGeoSphere.h"
68#include "TGeoTube.h"
69#include "TGeoVolume.h"
70
71#include <nlohmann/json.hpp>
72
73#include <algorithm>
74#include <array>
75#include <cctype>
76#include <chrono>
77#include <cmath>
78#include <cstdio>
79#include <fstream>
80#include <iostream>
81#include <memory>
82#include <set>
83#include <sstream>
84#include <string>
85#include <vector>
86
87using json = nlohmann::json;
88using namespace o2::cad;
89using namespace o2::cad::harness;
90using namespace o2::cad::xray;
91using namespace o2::cad::bench;
93
94namespace
95{
96
97// The xrays_/crossings_ JSON contract shared with Detectors/CADSupport/validation/xrayOracle.py. Bump on both
98// sides together; the oracle refuses a version it does not speak rather than guessing.
99constexpr int kXRayFormatVersion = 2;
100
101json comparisonToJson(const ListComparison& c)
102{
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}};
115}
116
117json robustnessToJson(const Robustness& r)
118{
119 return json{{"rays", r.rays},
120 {"raysWithCrossings", r.raysWithCrossings},
121 {"crossings", r.crossings},
122 {"steps", r.steps},
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}};
138}
139
140// ------------------------------------------------------------------------------------------
141// Mode (b): the real TGeoNavigator
142// ------------------------------------------------------------------------------------------
143
149class NavigatorTransport
150{
151 public:
164 NavigatorTransport(TGeoManager* manager, TGeoShape* shape, const Point3D& bboxMin,
165 const Point3D& bboxMax, const TGeoMatrix* placement = nullptr)
166 {
167 mManager = manager;
168 auto* material = new TGeoMaterial("Vacuum", 0., 0., 0.);
169 auto* medium = new TGeoMedium("Vacuum", 1, material);
170 double half[3];
171 double centre[3];
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;
175 }
176 auto* worldBox = new TGeoBBox("xrayWorld", half[0], half[1], half[2], centre);
177 mWorld = new TGeoVolume("TOP", worldBox, medium);
178 mPart = new TGeoVolume("PART", shape, medium);
179 // Identity unless the shape carries a placement, in which case this is where it is applied.
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();
185 }
186
188 ~NavigatorTransport() = default;
189
190 NavigatorTransport(const NavigatorTransport&) = delete;
191 NavigatorTransport& operator=(const NavigatorTransport&) = delete;
192
193 bool valid() const { return mNavigator != nullptr; }
194
195 std::vector<Crossing> transport(const Point3D& origin, const Point3D& dir, double tMax,
196 const StepConfig& cfg, Robustness& stats)
197 {
198 std::vector<Crossing> crossings;
199 mNavigator->InitTrack(origin.data(), dir.data());
200 if (mNavigator->IsOutside()) {
201 // The world is built to contain every ray of the raster, so this cannot fire on a correct
202 // configuration -- and it gets its own counter precisely so that a wrong one is never
203 // mistaken for a geometry defect.
204 ++stats.originOutsideWorld;
205 return crossings;
206 }
207 bool inPart = (mNavigator->GetCurrentVolume() == mPart);
208 if (inPart) {
209 ++stats.originInside;
210 }
211 int iter = 0;
212 for (; iter < cfg.maxIter; ++iter) {
213 const double* before = mNavigator->GetCurrentPoint();
214 double tBefore = 0.;
215 for (int k = 0; k < 3; ++k) {
216 tBefore += (before[k] - origin[k]) * dir[k];
217 }
218 mNavigator->FindNextBoundaryAndStep(TGeoShape::Big(), kFALSE);
219 const double step = mNavigator->GetStep();
220 ++stats.steps;
221 const double tCross = tBefore + step;
222 if (step <= cfg.zeroStep) {
223 ++stats.zeroLengthSteps;
224 }
225 if (!(tCross > tBefore)) {
226 ++stats.nonAdvancingSteps;
227 }
228 if (mNavigator->IsOutside() || tCross > tMax || !(step < TGeoShape::Big())) {
229 break;
230 }
231 const bool nowIn = (mNavigator->GetCurrentVolume() == mPart);
232 if (nowIn != inPart) {
233 crossings.push_back({tCross, nowIn ? +1 : -1});
234 inPart = nowIn;
235 } else {
236 ++stats.boundaryWithoutTransition;
237 }
238 }
239 if (iter >= cfg.maxIter) {
240 ++stats.iterationCapHits;
241 }
242 if (inPart) {
243 ++stats.unterminated;
244 }
245 return crossings;
246 }
247
248 private:
249 TGeoManager* mManager = nullptr;
250 TGeoVolume* mWorld = nullptr;
251 TGeoVolume* mPart = nullptr;
252 TGeoNavigator* mNavigator = nullptr;
253};
254
255// ------------------------------------------------------------------------------------------
256// Reading a crossing list, and comparing two of them
257// ------------------------------------------------------------------------------------------
258
259// ------------------------------------------------------------------------------------------
260// The raster
261// ------------------------------------------------------------------------------------------
262//
263// A structured parallel-beam raster, not Monte Carlo. Cell centres of an N x N lattice over the
264// raster window, one beam per axis. Structured wins for two independent reasons: the chord
265// integral converges far better than random sampling (boundary cells are the whole error budget
266// and their count grows as N rather than N^2), and a lattice deliberately produces the grazing,
267// edge-on and vertex-on rays a random direction essentially never generates -- which is where a
268// transport loop stalls.
269
270// ------------------------------------------------------------------------------------------
271// Options, part collection, IO
272// ------------------------------------------------------------------------------------------
273
274struct Options {
275 std::string db;
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;
286 int raster = 48;
287 std::string axesSpec = "xyz";
292 double margin = 1.e-3;
296 double tiltDegrees = 0.;
300 int fanBeams = 0;
301 std::string dumpRays;
302 std::string refCrossings;
303 std::string jsonOut;
307 std::set<std::string> representations = {"surface", "mesh", "shape"};
308 bool skipNavigator = false;
309 bool selfTest = false;
313 bool perf = false;
314 int perfPoints = 4096;
315 int perfRays = 4096;
316 int perfPasses = 9;
317 int perfWarmup = 2;
319 std::string ladderSpec;
320 StepConfig step;
321};
322
323struct Part {
324 std::string id;
325 std::string model;
326 std::string surfaces;
327 std::string facets;
328 std::string shape;
329 std::string flatcsg;
330};
331
335const std::string& sourceFor(const Part& part, const std::string& name)
336{
337 if (name == "surface") {
338 return part.surfaces;
339 }
340 if (name == "mesh") {
341 return part.facets;
342 }
343 if (name == "flatcsg") {
344 return part.flatcsg;
345 }
346 return part.shape;
347}
348
350const std::array<std::string, 4>& allRepresentations()
351{
352 static const std::array<std::string, 4> names{"surface", "mesh", "shape", "flatcsg"};
353 return names;
354}
355
356std::string deriveSidecarPath(const std::string& surfacesPath, const char* prefixOut,
357 const char* suffixOut)
358{
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) {
366 return {};
367 }
368 const std::string stem = base.substr(prefix.size(), base.size() - prefix.size() - suffix.size());
369 return dir + prefixOut + stem + suffixOut;
370}
371
372bool fileExists(const std::string& path)
373{
374 if (path.empty()) {
375 return false;
376 }
377 std::ifstream probe(path);
378 return static_cast<bool>(probe);
379}
380
382std::string sanitizePartId(const std::string& id)
383{
384 std::string out;
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 : '_');
388 }
389 return out;
390}
391
392void printUsage(const char* argv0)
393{
394 std::cout << "X-ray / geantino transport benchmark -- ordered crossing lists, by stepping.\n\n"
395 "Usage: "
396 << argv0 << " --db <dir> [--parts <substring>] [--raster N] [--axes xyz]\n"
397 " [--dump-rays D] [--ref-crossings D] [--json out.json]\n"
398 " or: "
399 << argv0 << " --surfaces <f> [--facets <f>] [--shape <f>] [--flatcsg <f>]\n"
400 " [options as above]\n"
401 " or: "
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"
443 " what does.\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"
451 " "
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"
455 " "
456 << argv0 << " --db <db> --ref-crossings /tmp/x --json /tmp/x/xray.json\n";
457}
458
459std::set<std::string> splitCsv(const std::string& s)
460{
461 std::set<std::string> out;
462 std::stringstream ss(s);
463 std::string tok;
464 while (std::getline(ss, tok, ',')) {
465 if (!tok.empty()) {
466 out.insert(tok);
467 }
468 }
469 return out;
470}
471
472bool parseArgs(int argc, char** argv, Options& opt)
473{
474 for (int i = 1; i < argc; ++i) {
475 const std::string a = argv[i];
476 auto next = [&](const char* flag) -> std::string {
477 if (i + 1 >= argc) {
478 throw std::runtime_error(std::string("missing value for ") + flag);
479 }
480 return argv[++i];
481 };
482 if (a == "--db") {
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") {
519 opt.perf = true;
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") {
539 opt.selfTest = true;
540 } else if (a == "-h" || a == "--help") {
541 printUsage(argv[0]);
542 return false;
543 } else {
544 throw std::runtime_error("unrecognized option: " + a);
545 }
546 }
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");
552 }
553 // Naming a sidecar means "score this", whatever the default set says.
554 if (!opt.explicitFlatCSG.empty()) {
555 opt.representations.insert("flatcsg");
556 }
557 return true;
558}
559
560std::vector<Part> collectParts(const Options& opt)
561{
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};
567 // The siblings are only DERIVED from a `surfaces_*.bin` stem. Naming a shape or a sidecar
568 // directly means "score exactly this", which is how one part is emitted two ways and the two
569 // scored against each other; guessing a third subject from that name would be inventing one.
570 if (!part.surfaces.empty()) {
571 if (part.facets.empty()) {
572 part.facets = deriveSidecarPath(part.surfaces, "facets_", ".bin");
573 }
574 if (part.shape.empty()) {
575 part.shape = deriveSidecarPath(part.surfaces, "shape_", ".root");
576 }
577 if (part.flatcsg.empty()) {
578 part.flatcsg = deriveSidecarPath(part.surfaces, "flatcsg_", ".bin");
579 }
580 }
581 parts.push_back(std::move(part));
582 return parts;
583 }
584 const std::string manifestPath = opt.db + "/manifest.json";
585 std::ifstream in(manifestPath);
586 if (!in) {
587 throw std::runtime_error("cannot open " + manifestPath);
588 }
589 json manifest;
590 in >> manifest;
591 for (const auto& p : manifest.at("parts")) {
592 Part part;
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");
600 }
601 part.flatcsg = p.value("flatcsg", std::string());
602 if (part.flatcsg.empty()) {
603 part.flatcsg = deriveSidecarPath(part.surfaces, "flatcsg_", ".bin");
604 }
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) {
609 continue;
610 }
611 }
612 parts.push_back(std::move(part));
613 }
614 return parts;
615}
616
617void writeRays(const std::string& dir, const std::string& partId, const Raster& raster,
618 const std::string& bboxSource)
619{
620 json doc;
621 doc["version"] = kXRayFormatVersion;
622 doc["part"] = partId;
623 doc["windowMin"] = {raster.windowMin[0], raster.windowMin[1], raster.windowMin[2]};
624 doc["windowMax"] = {raster.windowMax[0], raster.windowMax[1], raster.windowMax[2]};
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]}}});
629 }
630 doc["beams"] = beams;
631 doc["cellArea"] = raster.cellArea;
632 doc["transverseMargin"] = raster.transverseMargin;
633 doc["windowExcess"] = raster.windowExcess;
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]}},
639 {"tmax", r.tMax},
640 {"beam", r.beam}});
641 }
642 doc["rays"] = std::move(rays);
643 const std::string path = dir + "/xrays_" + sanitizePartId(partId) + ".json";
644 std::ofstream out(path);
645 if (!out) {
646 throw std::runtime_error("cannot write " + path);
647 }
648 out << doc.dump();
649 std::printf(" wrote %s (%zu rays)\n", path.c_str(), raster.rays.size());
650}
651
653struct OracleCrossings {
654 bool has = false;
655 double tolerance = 1.e-7;
656 double capacity = 0.;
657 double volumeChord = 0.;
658 bool valid = false;
659 std::vector<std::vector<Crossing>> perRay;
660 std::vector<bool> ambiguous;
661 long long ambiguousRays = 0;
662 Raster raster;
663};
664
665OracleCrossings loadOracleCrossings(const std::string& dir, const std::string& partId)
666{
667 OracleCrossings out;
668 const std::string path = dir + "/crossings_" + sanitizePartId(partId) + ".json";
669 std::ifstream in(path);
670 if (!in) {
671 return out;
672 }
673 json doc;
674 in >> doc;
675 if (doc.value("version", 0) != kXRayFormatVersion) {
676 throw std::runtime_error(path + ": unsupported format version");
677 }
678 out.has = true;
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>();
691 }
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")) {
695 Beam beam;
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>();
699 }
700 out.raster.beams.push_back(std::move(beam));
701 }
702 out.raster.rays.reserve(doc.at("rays").size());
703 for (const auto& r : doc.at("rays")) {
704 RayDef ray;
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>();
708 }
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>()});
717 }
718 out.perRay.push_back(std::move(crossings));
719 // A ray OCCT itself declined to classify somewhere along its length. Excluded from the
720 // comparison rather than scored either way -- the same treatment `nNoVerdict` gets in the
721 // sample gate, for the same reason: there is no ground truth to compare against there.
722 out.ambiguous.push_back(r.value("amb", false));
723 }
724 return out;
725}
726
733void toShapeFrame(const TGeoMatrix* placement, const Point3D& origin, const Point3D& dir,
734 Point3D& localOrigin, Point3D& localDir)
735{
736 if (placement == nullptr) {
737 localOrigin = origin;
738 localDir = dir;
739 return;
740 }
741 placement->MasterToLocal(origin.data(), localOrigin.data());
742 placement->MasterToLocalVect(dir.data(), localDir.data());
743}
744
748void placedBox(const TGeoBBox& box, const TGeoMatrix* placement, Point3D& lo, Point3D& hi)
749{
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];
754 }
755 if (placement == nullptr) {
756 return;
757 }
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]};
763 double master[3];
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]);
768 }
769 }
770 lo = outLo;
771 hi = outHi;
772}
773
778bool resolveBoundingBox(const Part& part, const Options& opt, Point3D& lo, Point3D& hi,
779 std::string& source)
780{
781 struct Candidate {
782 const char* name;
783 const std::string& path;
784 };
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)) {
791 continue;
792 }
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") {
797 auto* solid = new O2BVHSurfaceSolid(part.id.c_str());
798 if (LoadSurfaceSolid(candidate.path, *solid)) {
799 solid->CloseShape(true);
800 shape = solid;
801 }
802 } else if (std::string(candidate.name) == "mesh") {
803 auto* solid = new O2Tessellated(part.id.c_str());
804 if (LoadFacetSolid(candidate.path, *solid)) {
805 solid->CloseShape();
806 shape = solid;
807 }
808 } else if (std::string(candidate.name) == "flatcsg") {
809 auto* solid = new O2FlatCSG(part.id.c_str());
810 if (LoadFlatCSG(candidate.path, *solid)) {
811 solid->CloseShape();
812 shape = solid;
813 }
814 } else {
815 shape = loadShapeFromRootFile(candidate.path, nullptr);
816 // The window must be stated in the PART frame, so a placed shape's box is carried through
817 // its placement first. Skipping this would raster a rotated tube against the box of the tube
818 // at the origin -- a window that misses the part entirely.
819 placement.reset(loadShapePlacementFromRootFile(candidate.path));
820 }
821 const auto* box = dynamic_cast<const TGeoBBox*>(shape);
822 if (box != nullptr) {
823 placedBox(*box, placement.get(), lo, hi);
824 source = candidate.name;
825 delete manager;
826 gGeoManager = nullptr;
827 return true;
828 }
829 delete manager;
830 gGeoManager = nullptr;
831 }
832 return false;
833}
834
835// ------------------------------------------------------------------------------------------
836// --perf: per-call cost and memory, per representation, from one shared sample set
837// ------------------------------------------------------------------------------------------
838//
839// Everything here answers one question -- "what does asking this representation a navigation
840// question cost, and what does holding it cost" -- and it answers it under three constraints that
841// are the whole difference between a benchmark and a stopwatch:
842//
843// * SAME QUESTIONS. The point and ray sets are built once per part, from a designated reference
844// representation's own Contains(), and handed unchanged to all three. `partitionedBy` is
845// reported so nobody has to guess which one.
846// * WARM CACHE, and said so. Every kernel is warmed before it is timed and every part fits in
847// cache, so these are steady-state numbers for a single resident solid. A real simulation
848// holds thousands of solids and misses; the ratios here are an upper bound on how well the
849// cheaper representation does there, not a prediction of it.
850// * LOAD EXCLUDED FROM THE KERNEL, AND REPORTED SEPARATELY, because loading dominates the run
851// on a large model.
852
853json timingToJson(const TimingStat& t)
854{
855 json out{{"callsPerPass", t.callsPerPass},
856 {"passes", t.passes},
857 {"nsPerCallMedian", t.medianNsPerCall},
858 {"nsPerCallMin", t.minNsPerCall},
859 {"nsPerCallMax", t.maxNsPerCall},
860 {"spread", t.spread},
861 {"checksum", t.checksum}};
862 if (t.hitFraction >= 0.) {
863 out["hitFraction"] = t.hitFraction;
864 }
865 return out;
866}
867
869struct LoadedRep {
870 TGeoManager* manager = nullptr;
871 TGeoShape* shape = nullptr;
872 const O2BVHSurfaceSolid* surfaceSolid = nullptr;
873 const O2FlatCSG* flatSolid = nullptr;
874 std::unique_ptr<TGeoHMatrix> placement;
875 StructuralMemory structural;
876 MemorySnapshot loadDelta;
877 MemorySnapshot closeDelta;
878 double loadSeconds = 0.;
879 double closeSeconds = 0.;
880 bool meshClosedBody = true;
881 bool ok = false;
882};
883
889LoadedRep loadRepresentation(const std::string& name, const std::string& source,
890 const std::string& partId, int flatSplitDepth = -1,
891 double flatMinBoxFraction = -1.)
892{
893 LoadedRep rep;
894 rep.manager = new TGeoManager(("perf_" + name).c_str(), "representation benchmark");
895 rep.structural.sidecarBytes = fileBytes(source);
896 const MemorySnapshot before = readMemory();
897 const auto t0 = std::chrono::steady_clock::now();
898 if (name == "surface") {
899 auto* solid = new O2BVHSurfaceSolid(partId.c_str());
900 if (!LoadSurfaceSolid(source, *solid)) {
901 return rep;
902 }
903 const auto t1 = std::chrono::steady_clock::now();
904 rep.loadSeconds = std::chrono::duration<double>(t1 - t0).count();
905 rep.loadDelta = readMemory() - before;
906 const MemorySnapshot beforeClose = readMemory();
907 solid->CloseShape(true);
908 rep.closeSeconds = std::chrono::duration<double>(std::chrono::steady_clock::now() - t1).count();
909 rep.closeDelta = readMemory() - beforeClose;
910 rep.shape = solid;
911 rep.surfaceSolid = solid;
912 rep.structural.primitives = solid->GetNsurfaces();
913 // The patch count and the sidecar are the two EXACT numbers a surface solid has. The trim
914 // wires are variable-length per patch and live behind a private type, so the in-memory
915 // arithmetic is not available from outside; the sidecar bytes bound it from below and the
916 // measured heap delta bounds it from above, and both are printed rather than one guessed
917 // number in between.
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") {
923 auto* solid = new O2Tessellated(partId.c_str());
924 if (!LoadFacetSolid(source, *solid)) {
925 return rep;
926 }
927 const auto t1 = std::chrono::steady_clock::now();
928 rep.loadSeconds = std::chrono::duration<double>(t1 - t0).count();
929 rep.loadDelta = readMemory() - before;
930 const MemorySnapshot beforeClose = readMemory();
931 solid->CloseShape();
932 rep.closeSeconds = std::chrono::duration<double>(std::chrono::steady_clock::now() - t1).count();
933 rep.closeDelta = readMemory() - beforeClose;
934 rep.shape = solid;
935 rep.meshClosedBody = solid->IsClosedBody();
936 rep.structural.primitives = solid->GetNfacets();
937 // Exact, and the one representation whose in-memory size IS arithmetic: three index arrays
938 // per facet plus a deduplicated vertex array plus one outward normal per facet.
939 const long long nF = solid->GetNfacets();
940 const long long nV = solid->GetNvertices();
941 rep.structural.bytes = nV * static_cast<long long>(sizeof(O2Tessellated::Vertex_t)) +
942 nF * static_cast<long long>(sizeof(TGeoFacet)) +
943 nF * static_cast<long long>(sizeof(O2Tessellated::Vertex_t));
944 rep.structural.formula =
945 std::to_string(nV) + " vertices x " + std::to_string(sizeof(O2Tessellated::Vertex_t)) +
946 " B + " + std::to_string(nF) + " facets x " + std::to_string(sizeof(TGeoFacet)) +
947 " B + " + std::to_string(nF) + " normals x " + std::to_string(sizeof(O2Tessellated::Vertex_t)) + " B";
948 } else if (name == "flatcsg") {
949 auto* solid = new O2FlatCSG(partId.c_str());
950 if (!LoadFlatCSG(source, *solid)) {
951 return rep;
952 }
953 if (flatSplitDepth >= 0) {
954 solid->SetSplitDepth(flatSplitDepth);
955 }
956 if (flatMinBoxFraction >= 0.) {
957 solid->SetMinBoxFraction(flatMinBoxFraction);
958 }
959 const auto t1 = std::chrono::steady_clock::now();
960 rep.loadSeconds = std::chrono::duration<double>(t1 - t0).count();
961 rep.loadDelta = readMemory() - before;
962 const MemorySnapshot beforeClose = readMemory();
963 solid->CloseShape();
964 rep.closeSeconds = std::chrono::duration<double>(std::chrono::steady_clock::now() - t1).count();
965 rep.closeDelta = readMemory() - beforeClose;
966 if (!solid->IsClosed()) {
967 // A refused CloseShape leaves a shape that answers through its `_Loop` twins -- correct, and
968 // orders of magnitude slower. Timing it as if it were the accelerated path would be a
969 // measurement of the wrong thing, so the representation is dropped instead.
970 return rep;
971 }
972 rep.shape = solid;
973 rep.flatSolid = solid;
974 rep.structural.primitives = solid->GetNcells();
975 // Exact for everything this class owns: the halfspace blocks, the cell table, the sub-cell
976 // boxes with their concatenated active lists, and the BVH the class reports for itself.
977 const long long nH = solid->GetNhalfspaces();
978 const long long nC = solid->GetNcells();
979 const long long nB = solid->GetNboxes();
980 long long active = 0;
981 for (int i = 0; i < solid->GetNboxes(); ++i) {
982 active += solid->GetBox(i).nActive;
983 }
984 const long long bvh = static_cast<long long>(solid->GetBVHMemory());
985 rep.structural.bytes = nH * static_cast<long long>(sizeof(FlatCSGHalfspace)) +
986 nC * static_cast<long long>(sizeof(FlatCSGCell)) +
987 nC * 6 * static_cast<long long>(sizeof(double)) +
988 nB * static_cast<long long>(sizeof(FlatCSGBox)) +
989 active * static_cast<long long>(sizeof(int)) + bvh;
990 rep.structural.formula =
991 std::to_string(nH) + " halfspaces x " + std::to_string(sizeof(FlatCSGHalfspace)) + " B + " +
992 std::to_string(nC) + " cells x " + std::to_string(sizeof(FlatCSGCell) + 6 * sizeof(double)) +
993 " B + " + std::to_string(nB) + " boxes x " + std::to_string(sizeof(FlatCSGBox)) + " B + " +
994 std::to_string(active) + " active x " + std::to_string(sizeof(int)) + " B + BVH " +
995 std::to_string(bvh) + " B";
996 } else {
997 std::string error;
998 rep.shape = loadShapeFromRootFile(source, &error);
999 if (rep.shape == nullptr) {
1000 return rep;
1001 }
1002 rep.placement.reset(loadShapePlacementFromRootFile(source));
1003 rep.loadSeconds = std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count();
1004 rep.loadDelta = readMemory() - before;
1005 const BooleanTreeStats tree = booleanTreeStats(rep.shape);
1006 rep.structural.primitives = tree.leaves;
1007 // A composite is a handful of objects: the node count is exact and tiny, and that is the
1008 // headline of the whole memory column.
1009 rep.structural.bytes = tree.leaves * 200 + tree.nodes * 200;
1010 rep.structural.formula = "leaves=" + std::to_string(tree.leaves) +
1011 " nodes=" + std::to_string(tree.nodes) +
1012 " depth=" + std::to_string(tree.depth) +
1013 "; bytes ~ (leaves+nodes) x 200 B (ROOT object overhead dominates)";
1014 }
1015 rep.ok = true;
1016 return rep;
1017}
1018
1024QuerySamples toShapeFrame(const QuerySamples& in, const TGeoMatrix* placement)
1025{
1026 if (placement == nullptr) {
1027 return in;
1028 }
1029 QuerySamples out = in;
1030 for (auto& p : out.points) {
1031 Point3D q;
1032 placement->MasterToLocal(p.data(), q.data());
1033 p = q;
1034 }
1035 auto move = [&](std::vector<Ray>& rays) {
1036 for (auto& r : rays) {
1037 Point3D o;
1038 Point3D d;
1039 placement->MasterToLocal(r.origin.data(), o.data());
1040 placement->MasterToLocalVect(r.dir.data(), d.data());
1041 r.origin = o;
1042 r.dir = d;
1043 }
1044 };
1045 move(out.outsideRays);
1046 move(out.insideRays);
1047 return out;
1048}
1049
1058json localiseSurfaceSolid(const O2BVHSurfaceSolid* solid, const QuerySamples& s, int warmup, int passes)
1059{
1060 json out;
1061 const auto* shape = static_cast<const TGeoShape*>(solid);
1062 (void)shape;
1063
1064 const bool pruningWas = O2BVHSurfaceSolid::GetRayTMaxPruning();
1065
1068 for (const auto& ray : s.outsideRays) {
1069 volatile double sink = solid->DistFromOutside(ray.origin.data(), ray.dir.data(), 3, TGeoShape::Big(), nullptr);
1070 (void)sink;
1071 }
1072 const long long prunedCandidates = O2BVHSurfaceSolid::GetRayCandidateCount();
1073 const TimingStat pruned = timePasses(static_cast<long long>(s.outsideRays.size()), warmup, passes, [&]() {
1074 uint64_t acc = 0;
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);
1078 }
1079 return acc;
1080 });
1081
1084 for (const auto& ray : s.outsideRays) {
1085 volatile double sink = solid->DistFromOutside(ray.origin.data(), ray.dir.data(), 3, TGeoShape::Big(), nullptr);
1086 (void)sink;
1087 }
1088 const long long unprunedCandidates = O2BVHSurfaceSolid::GetRayCandidateCount();
1089 const TimingStat unpruned = timePasses(static_cast<long long>(s.outsideRays.size()), warmup, passes, [&]() {
1090 uint64_t acc = 0;
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);
1094 }
1095 return acc;
1096 });
1098
1099 const TimingStat loop = timePasses(static_cast<long long>(s.outsideRays.size()), warmup, passes, [&]() {
1100 uint64_t acc = 0;
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);
1104 }
1105 return acc;
1106 });
1107 const TimingStat containsLoop = timePasses(static_cast<long long>(s.points.size()), warmup, passes, [&]() {
1108 uint64_t acc = 0;
1109 for (const auto& p : s.points) {
1110 acc ^= solid->Contains_Loop(p.data()) ? 1u : 0u;
1111 }
1112 return acc;
1113 });
1114
1115 // --- the nearest-patch queries: Safety() and ComputeNormal() -------------------------------
1116 //
1117 // Same shape of measurement; the `_Loop` twins are the unaccelerated kernels, so "before" and
1118 // "after" run in the same binary on the same sample set.
1119 //
1120 // The disagreement counter travels with the timing on purpose: the twins must return bit-
1121 // identical answers, and a speed ratio quoted without it would price two different kernels.
1123 long long safetyDisagreements = 0;
1124 long long normalDisagreements = 0;
1125 for (size_t index = 0; index < s.points.size(); ++index) {
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;
1129 }
1130 Point3D viaBVH{0., 0., 0.};
1131 Point3D viaLoop{0., 0., 0.};
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;
1136 }
1137 }
1139 for (size_t index = 0; index < s.points.size(); ++index) {
1140 volatile double sink = solid->Safety(s.points[index].data(), s.pointIsInside[index] != 0);
1141 (void)sink;
1142 }
1143 const long long safetyCandidates = O2BVHSurfaceSolid::GetSafetyCandidateCount();
1144
1145 const TimingStat safetyBVH = timePasses(static_cast<long long>(s.points.size()), warmup, passes, [&]() {
1146 uint64_t acc = 0;
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);
1149 }
1150 return acc;
1151 });
1152 const TimingStat safetyLoop = timePasses(static_cast<long long>(s.points.size()), warmup, passes, [&]() {
1153 uint64_t acc = 0;
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);
1156 }
1157 return acc;
1158 });
1159 const TimingStat normalBVH = timePasses(static_cast<long long>(s.points.size()), warmup, passes, [&]() {
1160 uint64_t acc = 0;
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);
1165 }
1166 return acc;
1167 });
1168 const TimingStat normalLoop = timePasses(static_cast<long long>(s.points.size()), warmup, passes, [&]() {
1169 uint64_t acc = 0;
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);
1174 }
1175 return acc;
1176 });
1177
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()));
1180 out["safetyBVHNs"] = safetyBVH.medianNsPerCall;
1181 out["safetyLoopNs"] = safetyLoop.medianNsPerCall;
1182 out["safetySpeedup"] = safetyBVH.medianNsPerCall > 0. ? safetyLoop.medianNsPerCall / safetyBVH.medianNsPerCall : 0.;
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());
1190 std::printf(
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());
1196
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;
1202 out["distOutLoopNs"] = loop.medianNsPerCall;
1203 out["containsLoopNs"] = containsLoop.medianNsPerCall;
1204 out["nsPerBVHCandidate"] =
1205 prunedCandidates > 0 ? pruned.medianNsPerCall * rays / static_cast<double>(prunedCandidates) : 0.;
1206 std::printf(
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);
1213 return out;
1214}
1215
1216void printTiming(const char* label, const TimingStat& t)
1217{
1218 std::printf(" %-14s %9.1f ns/call [%9.1f .. %9.1f, spread %5.1f%%]", label,
1220 if (t.hitFraction >= 0.) {
1221 std::printf(" hit %5.1f%%", 100. * t.hitFraction);
1222 }
1223 std::printf("\n");
1224}
1225
1231json runLadder(const Options& opt)
1232{
1233 json out = json::array();
1234 std::vector<int> counts;
1235 {
1236 std::stringstream ss(opt.ladderSpec);
1237 std::string tok;
1238 while (std::getline(ss, tok, ',')) {
1239 if (!tok.empty()) {
1240 counts.push_back(std::stoi(tok));
1241 }
1242 }
1243 }
1244 std::printf("=== synthetic boolean ladder: unions of K overlapping TGeoTubes ===\n");
1245 std::printf(
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);
1253 const MemorySnapshot before = readMemory();
1254 TGeoShape* shape = buildBooleanLadder(k, shapeKind, tag);
1255 const MemorySnapshot after = readMemory();
1256 if (shape == nullptr) {
1257 delete manager;
1258 gGeoManager = nullptr;
1259 continue;
1260 }
1261 const BooleanTreeStats tree = booleanTreeStats(shape);
1262 const auto* box = dynamic_cast<const TGeoBBox*>(shape);
1263 const Point3D lo{box->GetOrigin()[0] - box->GetDX(), box->GetOrigin()[1] - box->GetDY(),
1264 box->GetOrigin()[2] - box->GetDZ()};
1265 const Point3D hi{box->GetOrigin()[0] + box->GetDX(), box->GetOrigin()[1] + box->GetDY(),
1266 box->GetOrigin()[2] + box->GetDZ()};
1267 const QuerySamples samples =
1268 buildQuerySamples(shape, "self", lo, hi, opt.perfPoints, opt.perfRays);
1269 const TimingStat contains = timeContainsPass(shape, samples, opt.perfWarmup, opt.perfPasses);
1270 const TimingStat safety = timeSafetyPass(shape, samples, opt.perfWarmup, opt.perfPasses);
1271 const TimingStat distOut = timeDistOutPass(shape, samples, opt.perfWarmup, opt.perfPasses);
1272 const TimingStat distIn = timeDistInPass(shape, samples, opt.perfWarmup, opt.perfPasses);
1273 std::printf(" --- K=%-3d %-9s (leaves=%lld nodes=%lld depth=%d, %.1f%% of points inside) ---\n",
1274 k, kindName, tree.leaves, tree.nodes, tree.depth,
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)}});
1294 delete manager;
1295 gGeoManager = nullptr;
1296 }
1297 }
1298 return out;
1299}
1300
1301// ------------------------------------------------------------------------------------------
1302// Self-test: analytic references, and the controls that prove the comparison can fail
1303// ------------------------------------------------------------------------------------------
1304
1305int selfTest()
1306{
1307 int failures = 0;
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());
1311 if (!ok) {
1312 ++failures;
1313 }
1314 };
1315
1316 StepConfig cfg;
1317 Robustness stats;
1318
1319 // 1. A box: exactly two crossings, at analytically known distances.
1320 {
1321 TGeoBBox box("selftestBox", 1., 1.5, 2.);
1322 const Point3D origin{-5., 0., 0.};
1323 const Point3D dir{1., 0., 0.};
1324 auto crossings = stepWithShapeApi(&box, origin, dir, 10., cfg, stats);
1325 check("box: exactly two crossings along a central ray", crossings.size() == 2,
1326 "got " + std::to_string(crossings.size()));
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);
1331 }
1332 }
1333
1334 // 2. A hollow tube: FOUR crossings along a diameter. This is the case a single-shot `distout`
1335 // query cannot express at all -- it reports the first of the four and stops.
1336 {
1337 TGeoTube tube("selftestTube", 0.5, 1.0, 2.0);
1338 const Point3D origin{-5., 0., 0.};
1339 const Point3D dir{1., 0., 0.};
1340 auto crossings = stepWithShapeApi(&tube, origin, dir, 10., cfg, stats);
1341 check("hollow tube: four crossings along a diameter", crossings.size() == 4,
1342 "got " + std::to_string(crossings.size()));
1343 if (crossings.size() == 4) {
1344 const double expect[4] = {4.0, 4.5, 5.5, 6.0};
1345 bool ok = true;
1346 for (int i = 0; i < 4; ++i) {
1347 ok = ok && std::fabs(crossings[i].t - expect[i]) < 1.e-9;
1348 }
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);
1353 }
1354 }
1355
1356 // 3a. A BOX's chord integral is EXACT, at every raster density, when the window is its own
1357 // bounding box. That is the sharpest available self-check on the volume instrument: no
1358 // convergence argument, no tolerance -- either the quadrature is the volume or it is not.
1359 // It is also what fixed the raster geometry: with the window inflated by 2 % instead, this
1360 // same box came out 5.1e-02 too large at N = 32.
1361 {
1362 TGeoBBox box("selftestVolBox", 1., 1.5, 2.);
1363 const Point3D bboxMin{-1., -1.5, -2.};
1364 const Point3D bboxMax{1., 1.5, 2.};
1365 for (const int n : {7, 32}) {
1366 Raster raster = buildRaster(bboxMin, bboxMax, n, buildBeams("xyz", 0.), 0.);
1367 Robustness s;
1368 std::vector<double> byAxis(3, 0.);
1369 for (const auto& ray : raster.rays) {
1370 const double before = s.insideLength;
1371 auto crossings = stepWithShapeApi(&box, ray.origin, ray.dir, ray.tMax, cfg, s);
1372 auditCrossingList(crossings, nullptr, ray.origin, ray.dir, ray.tMax, cfg, s);
1373 byAxis[ray.beam] += s.insideLength - before;
1374 }
1375 const double volume = chordVolume(raster, byAxis);
1376 check(("box 2 x 3 x 4 cm: chord integral is EXACT at N=" + std::to_string(n)).c_str(),
1377 std::fabs(volume - 24.) < 1.e-9, "got " + std::to_string(volume));
1378 }
1379 }
1380
1381 // 3b. A sphere's chord integral against its closed-form volume. A curved silhouette cannot be
1382 // exact at finite N, so this is where the ACHIEVED PRECISION of the volume instrument is
1383 // measured -- and the measurement says the convergence is NOT monotone in N (the silhouette
1384 // cells realign with the lattice at every density), so the honest statement is an envelope
1385 // at a stated density, never an extrapolation.
1386 {
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;
1391 double worst = 0.;
1392 for (const int n : {24, 48, 96, 192}) {
1393 Raster raster = buildRaster(bboxMin, bboxMax, n, buildBeams("z", 0.), 0.);
1394 Robustness s;
1395 for (const auto& ray : raster.rays) {
1396 auto crossings = stepWithShapeApi(&sphere, ray.origin, ray.dir, ray.tMax, cfg, s);
1397 auditCrossingList(crossings, nullptr, ray.origin, ray.dir, ray.tMax, cfg, s);
1398 }
1399 const double volume = s.insideLength * raster.cellArea[0];
1400 const double rel = std::fabs(volume - exact) / exact;
1401 worst = std::max(worst, rel);
1402 std::printf(
1403 " sphere r=1: raster %3d x %3d -> V = %.8f cm^3, exact %.8f, "
1404 "relative %.3e\n",
1405 n, n, volume, exact, rel);
1406 }
1407 // The bound is the MEASURED envelope over N = 24..192, not a convergence rate. If a future
1408 // change makes the quadrature worse than this it is a regression; if the envelope itself has
1409 // to be widened, that is a result to report rather than a constant to tune.
1410 check("sphere chord integral stays inside the measured 2e-3 envelope for N = 24..192",
1411 worst < 2.e-3, "worst rel=" + std::to_string(worst));
1412 }
1413
1414 // 4. THE CONTROLS. A comparison that cannot fail is not a comparison. Take a correct crossing
1415 // list and (i) perturb one distance, (ii) drop one crossing, (iii) duplicate one, and require
1416 // the comparator to name each.
1417 {
1418 const std::vector<Crossing> truth{{4.0, +1}, {4.5, -1}, {5.5, +1}, {6.0, -1}};
1419 const Point3D o{-5., 0., 0.};
1420 const Point3D d{1., 0., 0.};
1421
1422 ListComparison clean;
1423 compareLists(truth, truth, o, d, 1.e-6, clean);
1424 check("control 0: identical lists compare clean",
1425 clean.raysIdentical == 1 && clean.missing == 0 && clean.extra == 0 &&
1426 clean.matched == 4);
1427
1428 auto perturbed = truth;
1429 perturbed[2].t += 1.e-3;
1430 ListComparison shifted;
1431 compareLists(perturbed, truth, o, d, 1.e-6, shifted);
1432 check("control 1: a crossing moved by 1e-3 cm is CAUGHT, and as DISPLACED not as lost",
1433 shifted.raysIdentical == 0 && shifted.displaced == 1 && shifted.missing == 0 &&
1434 shifted.extra == 0 && std::fabs(shifted.worstDeltaT - 1.e-3) < 1.e-12,
1435 "displaced=" + std::to_string(shifted.displaced) + " missing=" +
1436 std::to_string(shifted.missing) + " dt=" + std::to_string(shifted.worstDeltaT));
1437
1438 auto dropped = truth;
1439 dropped.erase(dropped.begin() + 1);
1440 ListComparison lost;
1441 compareLists(dropped, truth, o, d, 1.e-6, lost);
1442 check("control 2: a dropped crossing is CAUGHT as `missing`",
1443 lost.missing == 1 && lost.extra == 0, "missing=" + std::to_string(lost.missing));
1444
1445 auto doubled = truth;
1446 doubled.insert(doubled.begin() + 1, {4.2, -1});
1447 ListComparison spurious;
1448 compareLists(doubled, truth, o, d, 1.e-6, spurious);
1449 check("control 3: an extra crossing is CAUGHT as `extra`",
1450 spurious.extra == 1 && spurious.missing == 0, "extra=" + std::to_string(spurious.extra));
1451
1452 // A crossing at the right place but with the wrong sense (enter where the truth exits) is a
1453 // different defect and must not be absorbed into `matched`.
1454 auto flipped = truth;
1455 flipped[1].kind = +1;
1456 ListComparison sense;
1457 compareLists(flipped, truth, o, d, 1.e-6, sense);
1458 check("control 4: a crossing with the wrong sense is CAUGHT", sense.kindMismatch == 1);
1459 }
1460
1461 // 5b. THE TIMING HARNESS'S OWN NEGATIVE CONTROL. A timing harness that cannot distinguish a
1462 // deliberately slowed shape from a fast one is not measuring what it claims. So:
1463 // time the same kernels on a TGeoBBox and on a TGeoBBox carrying ballast, and require the
1464 // number to MOVE, in the right direction, on all four kernels.
1465 {
1466 TGeoBBox fast("perfControlFast", 1., 1., 1.);
1467 BallastShape slow("perfControlSlow", 1., 1., 1., 60);
1468 const Point3D lo{-1., -1., -1.};
1469 const Point3D hi{1., 1., 1.};
1470 const QuerySamples samples = buildQuerySamples(&fast, "control", lo, hi, 2000, 2000);
1471 check("control 5: the shared sample set has both inside and outside points",
1472 samples.insidePoints > 100 &&
1473 samples.insidePoints < static_cast<long long>(samples.points.size()) - 100,
1474 "inside=" + std::to_string(samples.insidePoints) + " of " +
1475 std::to_string(samples.points.size()));
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)) {
1479 return false;
1480 }
1481 }
1482 return true;
1483 }());
1484 check("control 7: DistFromOutside rays actually hit (an all-miss set times the early-out)",
1485 timeDistOutPass(&fast, samples, 1, 3).hitFraction > 0.5);
1486
1487 struct Kernel {
1488 const char* name;
1489 TimingStat (*fn)(const TGeoShape*, const QuerySamples&, int, int);
1490 };
1491 const Kernel kernels[4] = {{"Contains", &timeContainsPass},
1492 {"Safety", &timeSafetyPass},
1493 {"DistFromOutside", &timeDistOutPass},
1494 {"DistFromInside", &timeDistInPass}};
1495 for (const auto& kernel : kernels) {
1496 const TimingStat quick = kernel.fn(&fast, samples, 2, 7);
1497 const TimingStat heavy = kernel.fn(&slow, samples, 2, 7);
1498 const double ratio = quick.medianNsPerCall > 0. ? heavy.medianNsPerCall / quick.medianNsPerCall : 0.;
1499 check((std::string("control 8: ballast is VISIBLE on ") + kernel.name +
1500 " (the timing harness can move its own number)")
1501 .c_str(),
1502 ratio > 2.,
1503 std::string("fast=") + std::to_string(quick.medianNsPerCall) + " ns slow=" +
1504 std::to_string(heavy.medianNsPerCall) + " ns ratio=" + std::to_string(ratio));
1505 check((std::string("control 9: the ") + kernel.name +
1506 " timing loop was not elided (non-zero checksum, positive time)")
1507 .c_str(),
1508 quick.checksum != 0 && quick.medianNsPerCall > 0. && quick.passes == 7);
1509 }
1510 }
1511
1512 // 5c. THE MEMORY PROBE'S NEGATIVE CONTROL. Both memory numbers must move when memory is taken,
1513 // and the heap number must come back when it is given up. Without this the "resident delta"
1514 // column could be reporting allocator noise and nobody would know.
1515 {
1516 const MemorySnapshot before = readMemory();
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); // touch every page: an untouched mmap is not resident
1521 }
1522 const MemorySnapshot held = readMemory();
1523 const MemorySnapshot delta = held - before;
1524 check("control 10: the resident probe sees a 64 MB touched allocation",
1525 delta.residentBytes > 32LL << 20,
1526 "delta=" + std::to_string(delta.residentBytes >> 20) + " MB");
1527 check("control 11: the heap probe sees a 64 MB allocation",
1528 delta.heapInUseBytes > 32LL << 20,
1529 "delta=" + std::to_string(delta.heapInUseBytes >> 20) + " MB");
1530 delete[] block;
1531 const MemorySnapshot released = readMemory() - before;
1532 check("control 12: the heap probe sees it released again (the resident one need not)",
1533 released.heapInUseBytes < 8LL << 20,
1534 "still=" + std::to_string(released.heapInUseBytes >> 20) + " MB");
1535 }
1536
1537 // 5d. THE STRUCTURAL MEMORY CONTROL. The exact column has to depend on the geometry, so build
1538 // the same tree twice at different sizes and require the count -- and the derived byte
1539 // figure -- to follow. A structural number that does not move with the structure is a
1540 // constant with a units label.
1541 {
1542 auto* manager = new TGeoManager("perfControlLadder", "structural control");
1543 TGeoShape* small = buildBooleanLadder(4, LadderShape::Balanced, "ctlS");
1544 TGeoShape* big = buildBooleanLadder(32, LadderShape::Balanced, "ctlB");
1545 const BooleanTreeStats a = booleanTreeStats(small);
1546 const BooleanTreeStats b = booleanTreeStats(big);
1547 check("control 13: the ladder builds the leaf count it was asked for",
1548 a.leaves == 4 && b.leaves == 32,
1549 "got " + std::to_string(a.leaves) + " and " + std::to_string(b.leaves));
1550 check("control 14: a balanced ladder's depth is logarithmic in its leaf count",
1551 a.depth == 3 && b.depth == 6,
1552 "depth " + std::to_string(a.depth) + " and " + std::to_string(b.depth));
1553 TGeoShape* chain = buildBooleanLadder(32, LadderShape::Chain, "ctlC");
1555 check(
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,
1559 "leaves=" + std::to_string(c.leaves) + " depth=" + std::to_string(c.depth));
1560 delete manager;
1561 gGeoManager = nullptr;
1562 }
1563
1564 // 5. The parity audit's own control: hand it a list with a crossing removed and require the
1565 // midpoint Contains() check to contradict it.
1566 {
1567 TGeoBBox box("selftestBox2", 1., 1., 1.);
1568 const Point3D origin{-5., 0., 0.};
1569 const Point3D dir{1., 0., 0.};
1570 Robustness good;
1571 auditCrossingList({{4.0, +1}, {6.0, -1}}, &box, origin, dir, 10., cfg, good);
1572 check("parity audit: a correct list has no parity mismatch", good.parityMismatchIntervals == 0);
1573 Robustness bad;
1574 auditCrossingList({{4.0, +1}}, &box, origin, dir, 10., cfg, bad);
1575 check("parity audit: a truncated list is CAUGHT by Contains() at the midpoints",
1576 bad.parityMismatchIntervals > 0 && bad.oddCrossingLists == 1);
1577 }
1578
1579 std::printf("\n%s: %d failure(s)\n", failures == 0 ? "SELF-TEST PASSED" : "SELF-TEST FAILED",
1580 failures);
1581 return failures == 0 ? 0 : 1;
1582}
1583
1584} // namespace
1585
1586int main(int argc, char** argv)
1587{
1588 Options opt;
1589 try {
1590 if (!parseArgs(argc, argv, opt)) {
1591 return 0;
1592 }
1593 } catch (const std::exception& e) {
1594 std::cerr << "error: " << e.what() << "\n";
1595 printUsage(argv[0]);
1596 return 1;
1597 }
1598
1599 if (opt.selfTest) {
1600 return selfTest();
1601 }
1602
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());
1609 }
1610 return 0;
1611 }
1612
1613 std::vector<Beam> beams;
1614 std::vector<Part> parts;
1615 try {
1616 beams = opt.fanBeams > 0 ? buildFanBeams(opt.fanBeams)
1617 : buildBeams(opt.axesSpec, opt.tiltDegrees);
1618 if (beams.empty()) {
1619 throw std::runtime_error("no beam selected (--axes)");
1620 }
1621 parts = collectParts(opt);
1622 } catch (const std::exception& e) {
1623 std::cerr << "error: " << e.what() << "\n";
1624 return 1;
1625 }
1626 if (parts.empty()) {
1627 std::cerr << "no parts matched (pattern='" << opt.partsPattern << "')\n";
1628 return 1;
1629 }
1630
1631 json report = json::array();
1632
1633 // ---- --perf: the representation cost/memory comparison ---------------------------------
1634 if (opt.perf) {
1635 std::printf(
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());
1641 Point3D lo{};
1642 Point3D hi{};
1643 std::string bboxSource;
1644 if (!resolveBoundingBox(part, opt, lo, hi, bboxSource)) {
1645 std::printf(" skip: no representation could supply a bounding box\n");
1646 continue;
1647 }
1648 const Raster raster = buildRaster(lo, hi, opt.raster, beams, opt.margin);
1649
1650 // The sample set is built ONCE, from the first representation present in the order
1651 // surface -> mesh -> shape, and every representation is then asked exactly it. The order is
1652 // a preference for the representation whose Contains() is exact, not an accident: the
1653 // partition is a fixed label, so it wants to come from the most trustworthy classifier
1654 // available, and it is reported either way.
1656 std::string partitionedBy;
1657 for (const auto& candidate : allRepresentations()) {
1658 const std::string& source = sourceFor(part, candidate);
1659 if (!opt.representations.count(candidate) || !fileExists(source)) {
1660 continue;
1661 }
1662 LoadedRep rep = loadRepresentation(candidate, source, part.id, opt.flatSplitDepth,
1663 opt.flatMinBoxFraction);
1664 if (rep.ok) {
1665 // Points are drawn in the PART frame; a placed shape classifies them in its own.
1666 QuerySamples inFrame =
1667 buildQuerySamples(rep.shape, candidate, lo, hi, opt.perfPoints, opt.perfRays);
1668 if (rep.placement) {
1669 // Undo the frame so the stored set is the part frame's, as every other consumer
1670 // expects. Drawing in the shape frame and unmapping is equivalent and simpler than
1671 // threading the matrix through the generator.
1672 for (auto& p : inFrame.points) {
1673 Point3D q;
1674 rep.placement->LocalToMaster(p.data(), q.data());
1675 p = q;
1676 }
1677 for (auto* rays : {&inFrame.outsideRays, &inFrame.insideRays}) {
1678 for (auto& r : *rays) {
1679 Point3D o;
1680 Point3D d;
1681 rep.placement->LocalToMaster(r.origin.data(), o.data());
1682 rep.placement->LocalToMasterVect(r.dir.data(), d.data());
1683 r.origin = o;
1684 r.dir = d;
1685 }
1686 }
1687 }
1688 samples = std::move(inFrame);
1689 partitionedBy = candidate;
1690 }
1691 delete rep.manager;
1692 gGeoManager = nullptr;
1693 if (!partitionedBy.empty()) {
1694 break;
1695 }
1696 }
1697 if (partitionedBy.empty()) {
1698 std::printf(" skip: no representation loaded\n");
1699 continue;
1700 }
1701 samples.partitionedBy = partitionedBy;
1702 std::printf(
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",
1705 samples.points.size(),
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());
1710
1711 json partJson;
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();
1719
1720 for (const auto& candidate : allRepresentations()) {
1721 const std::string& source = sourceFor(part, candidate);
1722 if (!opt.representations.count(candidate) || !fileExists(source)) {
1723 continue;
1724 }
1725 LoadedRep rep = loadRepresentation(candidate, source, part.id, opt.flatSplitDepth,
1726 opt.flatMinBoxFraction);
1727 if (!rep.ok) {
1728 std::printf(" [skip %s] would not load from %s\n", candidate.c_str(), source.c_str());
1729 delete rep.manager;
1730 gGeoManager = nullptr;
1731 continue;
1732 }
1733 const QuerySamples local = toShapeFrame(samples, rep.placement.get());
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"
1739 : "leaves",
1740 rep.loadSeconds, rep.closeSeconds);
1741
1742 const TimingStat contains = timeContainsPass(rep.shape, local, opt.perfWarmup, opt.perfPasses);
1743 const TimingStat safety = timeSafetyPass(rep.shape, local, opt.perfWarmup, opt.perfPasses);
1744 const TimingStat distOut = timeDistOutPass(rep.shape, local, opt.perfWarmup, opt.perfPasses);
1745 const TimingStat distIn = timeDistInPass(rep.shape, local, opt.perfWarmup, opt.perfPasses);
1746 printTiming("Contains", contains);
1747 printTiming("Safety", safety);
1748 printTiming("DistFromOutside", distOut);
1749 printTiming("DistFromInside", distIn);
1750
1751 // Full geantino transport over the raster, timed the same way: several complete passes,
1752 // median reported. This is the number a simulation actually pays, and it is the only one
1753 // that composes the four kernels in the order a transport does.
1754 Robustness statsTransport;
1755 long long crossings = 0;
1756 const TimingStat transport =
1757 timePasses(static_cast<long long>(raster.rays.size()), opt.perfWarmup, opt.perfPasses, [&]() {
1758 uint64_t acc = 0;
1759 Robustness s;
1760 long long found = 0;
1761 for (const auto& ray : raster.rays) {
1762 Point3D o;
1763 Point3D d;
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());
1767 acc += list.size();
1768 }
1769 statsTransport = s;
1770 crossings = found;
1771 return acc;
1772 });
1773 // `crossings` is set from the last pass; every pass sees the same rays, so it is the
1774 // per-pass crossing count and the ns/crossing below is exact rather than averaged over a
1775 // varying denominator. It is counted from the returned lists rather than from the
1776 // Robustness bookkeeping, which only fills in `crossings` when the per-ray audit runs --
1777 // and the audit is deliberately NOT run inside a timed pass, because Contains() at every
1778 // interval midpoint would put a fifth kernel into a transport measurement.
1779 const double nsPerCrossing =
1780 crossings > 0 ? transport.medianNsPerCall * static_cast<double>(raster.rays.size()) /
1781 static_cast<double>(crossings)
1782 : 0.;
1783 std::printf(
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);
1789
1790 const MemorySnapshot total{rep.loadDelta.residentBytes + rep.closeDelta.residentBytes,
1791 rep.loadDelta.heapInUseBytes + rep.closeDelta.heapInUseBytes};
1792 std::printf(
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) {
1800 std::printf(
1801 " *** meshClosedBody = FALSE: this mesh is INVALID, not merely "
1802 "inaccurate. Read no accuracy column of this row as a safety statement. ***\n");
1803 }
1804
1805 json repJson;
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;
1833 }
1834 if (rep.surfaceSolid != nullptr) {
1835 repJson["localise"] = localiseSurfaceSolid(rep.surfaceSolid, local, opt.perfWarmup,
1836 opt.perfPasses);
1837 }
1838 if (rep.flatSolid != nullptr) {
1839 // The two counts the crossover is regressed against (Design_FlatCSGSolid.md section 9),
1840 // plus the box structure the split knobs move.
1841 long long active = 0;
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;
1845 active += n;
1846 worst = std::max(worst, n);
1847 }
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())
1855 : 0.;
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;
1861 }
1862 repsJson.push_back(std::move(repJson));
1863 delete rep.manager;
1864 gGeoManager = nullptr;
1865 }
1866 partJson["representations"] = std::move(repsJson);
1867 report.push_back(std::move(partJson));
1868 std::printf("\n");
1869 }
1870 if (!opt.jsonOut.empty()) {
1871 std::ofstream out(opt.jsonOut);
1872 out << report.dump(1);
1873 std::printf("\nreport: %s\n", opt.jsonOut.c_str());
1874 }
1875 return 0;
1876 }
1877
1878 for (const auto& part : parts) {
1879 std::printf("=== %s (%s) ===\n", part.id.c_str(), part.model.c_str());
1880 json partJson;
1881 partJson["id"] = part.id;
1882 partJson["model"] = part.model;
1883
1884 // ---- the raster window -------------------------------------------------------------
1885 // In scoring mode it comes from the oracle's answer file, so the two sides cannot possibly be
1886 // asking about different rays; otherwise it is built here from the tightest containing
1887 // bounding box the part has.
1888 Raster raster;
1889 OracleCrossings oracle;
1890 std::string bboxSource = "?";
1891 if (!opt.refCrossings.empty()) {
1892 try {
1893 oracle = loadOracleCrossings(opt.refCrossings, part.id);
1894 } catch (const std::exception& e) {
1895 std::cerr << " error reading crossings: " << e.what() << "\n";
1896 continue;
1897 }
1898 if (!oracle.has) {
1899 std::printf(" skip: no crossings file for this part in %s\n", opt.refCrossings.c_str());
1900 continue;
1901 }
1902 raster = oracle.raster;
1903 opt.step.matchTolerance = std::max(oracle.tolerance, 1.e-6);
1904 std::printf(
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);
1909 } else {
1910 Point3D lo{};
1911 Point3D hi{};
1912 if (!resolveBoundingBox(part, opt, lo, hi, bboxSource)) {
1913 std::printf(" skip: no representation could supply a bounding box\n");
1914 continue;
1915 }
1916 raster = buildRaster(lo, hi, opt.raster, beams, opt.margin);
1917 std::printf(
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,
1921 bboxSource.c_str(), raster.transverseMargin, raster.windowExcess.front());
1922 }
1923
1924 // `--dump-rays` writes the raster and stops: the oracle answers it next, and the scoring pass
1925 // then reads the rays back from the oracle's file. Nothing is stepped here.
1926 if (!opt.dumpRays.empty()) {
1927 writeRays(opt.dumpRays, part.id, raster, bboxSource);
1928 continue;
1929 }
1930
1931 // ---- representations ---------------------------------------------------------------
1932 struct RepSpec {
1933 std::string name;
1934 std::string source;
1935 };
1936 std::vector<RepSpec> specs;
1937 if (opt.representations.count("surface") && fileExists(part.surfaces)) {
1938 specs.push_back({"surface", part.surfaces});
1939 }
1940 if (opt.representations.count("mesh") && fileExists(part.facets)) {
1941 specs.push_back({"mesh", part.facets});
1942 }
1943 if (opt.representations.count("shape") && fileExists(part.shape)) {
1944 specs.push_back({"shape", part.shape});
1945 }
1946 if (opt.representations.count("flatcsg") && fileExists(part.flatcsg)) {
1947 specs.push_back({"flatcsg", part.flatcsg});
1948 }
1949 if (specs.empty()) {
1950 std::printf(" skip: no representation available\n");
1951 continue;
1952 }
1953
1954 json repsJson = json::array();
1955
1956 for (const auto& spec : specs) {
1957 // A fresh TGeoManager per representation: it owns the shape (TGeoShape registers itself in
1958 // gGeoManager on construction, so any other arrangement double-frees) and it carries the
1959 // one-part world mode (b) transports through.
1960 auto* manager = new TGeoManager(("xray_" + spec.name).c_str(), "X-ray benchmark world");
1961 TGeoShape* shape = nullptr;
1962 // The shape's own frame, when it is not the part frame. Mode (a) transforms each ray into
1963 // it; mode (b) puts it on the node. Owned here: the manager owns the shape, not the matrix.
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") {
1970 auto* solid = new O2BVHSurfaceSolid(part.id.c_str());
1971 if (!LoadSurfaceSolid(spec.source, *solid)) {
1972 std::printf(" [skip %s] LoadSurfaceSolid failed for %s\n", spec.name.c_str(),
1973 spec.source.c_str());
1974 delete manager;
1975 gGeoManager = nullptr;
1976 continue;
1977 }
1978 solid->CloseShape(true);
1979 primitives = solid->GetNsurfaces();
1980 primitiveKind = "patches";
1981 shape = solid;
1982 } else if (spec.name == "mesh") {
1983 auto* solid = new O2Tessellated(part.id.c_str());
1984 if (!LoadFacetSolid(spec.source, *solid)) {
1985 std::printf(" [skip %s] LoadFacetSolid failed for %s\n", spec.name.c_str(),
1986 spec.source.c_str());
1987 delete manager;
1988 gGeoManager = nullptr;
1989 continue;
1990 }
1991 solid->CloseShape();
1992 primitives = solid->GetNfacets();
1993 primitiveKind = "triangles";
1994 shape = solid;
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);
1999 }
2000 if (opt.flatMinBoxFraction >= 0.) {
2001 solid->SetMinBoxFraction(opt.flatMinBoxFraction);
2002 }
2003 if (!LoadFlatCSG(spec.source, *solid)) {
2004 std::printf(" [skip %s] LoadFlatCSG failed for %s\n", spec.name.c_str(),
2005 spec.source.c_str());
2006 delete manager;
2007 gGeoManager = nullptr;
2008 continue;
2009 }
2010 solid->CloseShape();
2011 if (!solid->IsClosed()) {
2012 std::printf(
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());
2016 delete manager;
2017 gGeoManager = nullptr;
2018 continue;
2019 }
2020 primitives = solid->GetNcells();
2021 primitiveKind = "cells";
2022 shape = solid;
2023 } else {
2024 std::string error;
2025 shape = loadShapeFromRootFile(spec.source, &error);
2026 if (shape == nullptr) {
2027 std::printf(" [skip %s] %s\n", spec.name.c_str(), error.c_str());
2028 delete manager;
2029 gGeoManager = nullptr;
2030 continue;
2031 }
2032 placement.reset(loadShapePlacementFromRootFile(spec.source));
2033 primitiveKind = shape->ClassName();
2034 }
2035 loadSeconds = std::chrono::duration<double>(std::chrono::steady_clock::now() - tLoad0).count();
2036
2037 const auto* box = dynamic_cast<const TGeoBBox*>(shape);
2038
2039 std::printf(" --- %-8s %-28s (%d %s, load %.3f s) ---\n", spec.name.c_str(),
2040 shape->ClassName(), primitives, primitiveKind, loadSeconds);
2041
2042 json repJson;
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);
2051
2052 // ---- mode (a): the shape API ------------------------------------------------------
2053 Robustness statsA;
2054 std::vector<double> insideByAxisA(raster.beams.size(), 0.);
2055 std::vector<std::vector<Crossing>> listsA(raster.rays.size());
2056 ListComparison vsOracleA;
2057 {
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];
2061 const double before = statsA.insideLength;
2062 Point3D o;
2063 Point3D d;
2064 toShapeFrame(placement.get(), ray.origin, ray.dir, o, d);
2065 listsA[i] = stepWithShapeApi(shape, o, d, ray.tMax, opt.step, statsA);
2066 auditCrossingList(listsA[i], shape, o, d, ray.tMax, opt.step, statsA);
2067 insideByAxisA[ray.beam] += statsA.insideLength - before;
2068 }
2069 statsA.seconds = std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count();
2070 }
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) {
2075 perAxisA[raster.beams[b].label] = insideByAxisA[b] * raster.cellArea[b];
2076 }
2077 repJson["modeA"]["volumeChordPerAxisCm3"] = perAxisA;
2078 if (oracle.has) {
2079 for (size_t i = 0; i < raster.rays.size() && i < oracle.perRay.size(); ++i) {
2080 if (oracle.ambiguous[i]) {
2081 continue; // OCCT declined somewhere along this ray; there is no ground truth to score
2082 }
2083 compareLists(listsA[i], oracle.perRay[i], raster.rays[i].origin, raster.rays[i].dir,
2084 opt.step.matchTolerance, vsOracleA);
2085 }
2086 repJson["modeA"]["vsOracle"] = comparisonToJson(vsOracleA);
2087 }
2088 std::printf(
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",
2091 statsA.rays, statsA.crossings, statsA.seconds, statsA.zeroLengthSteps,
2092 statsA.unstickPushes, statsA.nonAdvancingSteps, statsA.iterationCapHits,
2093 statsA.unterminated, statsA.oddCrossingLists, statsA.duplicateCrossings,
2095 if (oracle.has) {
2096 std::printf(
2097 " vs OCCT : %lld/%lld rays identical, LOST=%lld extra=%lld "
2098 "displaced=%lld kind=%lld worst dt=%.3g cm\n",
2099 vsOracleA.raysIdentical, vsOracleA.rays, vsOracleA.missing, vsOracleA.extra,
2100 vsOracleA.displaced, vsOracleA.kindMismatch, vsOracleA.worstDeltaT);
2101 if (!vsOracleA.worstReason.empty() && vsOracleA.worstReason != "deltaT") {
2102 std::printf(" worst : %s at o=(%.6g, %.6g, %.6g) d=(%.4g, %.4g, %.4g)\n",
2103 vsOracleA.worstReason.c_str(), vsOracleA.worstOrigin[0],
2104 vsOracleA.worstOrigin[1], vsOracleA.worstOrigin[2], vsOracleA.worstDir[0],
2105 vsOracleA.worstDir[1], vsOracleA.worstDir[2]);
2106 }
2107 }
2108 std::printf(" volume : chord integral %.8g cm^3 (Capacity %.8g)\n",
2109 repJson["modeA"]["volumeChordCm3"].get<double>(), shape->Capacity());
2110
2111 // ---- mode (b): the real navigator -------------------------------------------------
2112 if (!opt.skipNavigator && box != nullptr) {
2113 Robustness statsB;
2114 std::vector<double> insideByAxisB(raster.beams.size(), 0.);
2115 ListComparison vsOracleB;
2116 ListComparison aVsB;
2117 // The world must contain the part AND every ray of the raster, start to finish. Deriving
2118 // it from the axis-aligned window is not enough once the beams are tilted: a rotated
2119 // lattice reaches outside the part's own box, and the first version of this loop reported
2120 // 5358 lost crossings at a 27 degree tilt that were entirely its own undersized world.
2121 Point3D wMin;
2122 Point3D wMax;
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});
2129 }
2130 }
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];
2135 const double before = statsB.insideLength;
2136 auto listB = transport.transport(ray.origin, ray.dir, ray.tMax, opt.step, statsB);
2137 // The shape is handed in here as well, deliberately: in mode (b) the parity audit
2138 // compares the NAVIGATOR's crossing list against the SHAPE's own Contains(), which is a
2139 // genuine cross-check between the two and not a tautology.
2140 Point3D o;
2141 Point3D d;
2142 toShapeFrame(placement.get(), ray.origin, ray.dir, o, d);
2143 auditCrossingList(listB, shape, o, d, ray.tMax, opt.step, statsB);
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,
2147 vsOracleB);
2148 }
2149 compareLists(listB, listsA[i], ray.origin, ray.dir, opt.step.matchTolerance, aVsB);
2150 }
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);
2154 if (oracle.has) {
2155 repJson["modeB"]["vsOracle"] = comparisonToJson(vsOracleB);
2156 }
2157 repJson["modeAvsB"] = comparisonToJson(aVsB);
2158 std::printf(
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",
2161 statsB.rays, statsB.crossings, statsB.seconds, statsB.zeroLengthSteps,
2162 statsB.nonAdvancingSteps, statsB.iterationCapHits, statsB.unterminated,
2163 statsB.oddCrossingLists, statsB.duplicateCrossings,
2165 if (oracle.has) {
2166 std::printf(
2167 " vs OCCT : %lld/%lld rays identical, LOST=%lld extra=%lld "
2168 "displaced=%lld worst dt=%.3g cm\n",
2169 vsOracleB.raysIdentical, vsOracleB.rays, vsOracleB.missing, vsOracleB.extra,
2170 vsOracleB.displaced, vsOracleB.worstDeltaT);
2171 }
2172 std::printf(
2173 " (a)vs(b): %lld/%lld rays identical, LOST=%lld extra=%lld "
2174 "displaced=%lld worst dt=%.3g cm\n",
2175 aVsB.raysIdentical, aVsB.rays, aVsB.missing, aVsB.extra, aVsB.displaced,
2176 aVsB.worstDeltaT);
2177 std::printf(" volume : chord integral %.8g cm^3\n",
2178 repJson["modeB"]["volumeChordCm3"].get<double>());
2179 }
2180
2181 repsJson.push_back(std::move(repJson));
2182 delete manager; // frees the shape, the world and the navigator with it
2183 gGeoManager = nullptr;
2184 }
2185
2186 partJson["raster"] = {{"n", raster.n},
2187 {"rays", raster.rays.size()},
2188 {"cellArea", raster.cellArea},
2189 {"transverseMargin", raster.transverseMargin},
2190 {"windowExcess", raster.windowExcess},
2191 {"windowMin", {raster.windowMin[0], raster.windowMin[1], raster.windowMin[2]}},
2192 {"windowMax", {raster.windowMax[0], raster.windowMax[1], raster.windowMax[2]}}};
2193 if (oracle.has) {
2194 // Three volume numbers, and they answer three different questions. `volumeChordCm3` is
2195 // OCCT's OWN chord integral over these same rays, so comparing a candidate against it is
2196 // immune to the raster's own error; `capacity` is OCCT's exact volume, so
2197 // (oracle chord - capacity) IS the raster's achieved precision, measured at this density;
2198 // and each representation's `capacity` is the number the sample gate already scores.
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
2205 : 0.},
2206 {"ambiguousRays", oracle.ambiguousRays},
2207 {"valid", oracle.valid}};
2208 std::printf(
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());
2214 }
2215 partJson["representations"] = std::move(repsJson);
2216 report.push_back(std::move(partJson));
2217 }
2218
2219 if (!opt.jsonOut.empty()) {
2220 std::ofstream out(opt.jsonOut);
2221 out << report.dump(1);
2222 std::printf("\nreport: %s\n", opt.jsonOut.c_str());
2223 }
2224 return 0;
2225}
header::DataOrigin origin
int32_t i
GPUChain * chain
GPUTPCCFCheckPadBaseline Kernel
bool valid
Validation and timing harness for TGeoShape navigation, typed on plain TGeoShape*.
bool fileExists(const char *filename)
uint32_t c
Definition RawData.h:2
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 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.
GLdouble n
Definition glcorearb.h:1982
GLsizeiptr size
Definition glcorearb.h:659
GLuint GLuint end
Definition glcorearb.h:469
GLuint index
Definition glcorearb.h:781
GLuint const GLchar * name
Definition glcorearb.h:781
GLsizei samples
Definition glcorearb.h:1309
GLboolean GLboolean GLboolean b
Definition glcorearb.h:1233
GLsizei GLsizei GLchar * source
Definition glcorearb.h:798
GLuint GLsizei const GLchar * label
Definition glcorearb.h:2519
typedef void(APIENTRYP PFNGLCULLFACEPROC)(GLenum mode)
GLsizei const GLchar *const * path
Definition glcorearb.h:3591
GLboolean r
Definition glcorearb.h:1233
GLuint GLfloat GLfloat GLfloat GLfloat GLfloat GLfloat GLfloat t0
Definition glcorearb.h:5034
GLboolean GLboolean GLboolean GLboolean a
Definition glcorearb.h:1233
GLuint id
Definition glcorearb.h:650
GLsizei const GLint * box
Definition glcorearb.h:4697
GLuint GLfloat GLfloat GLfloat GLfloat GLfloat GLfloat GLfloat GLfloat GLfloat t1
Definition glcorearb.h:5034
void report(gsl::span< o2::InteractionTimeRecord > irs, int threshold, bool verbose)
bool contains(bvh::v2::BBox< T, 3 > const &box, bvh::v2::Vec< T, 3 > const &p)
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)
Definition common.h:52
void empty(int)
nlohmann::json json
One DNF cell: [first, first + count) of the halfspace array, intersected; volume is its own volume.
Definition O2FlatCSG.h:37
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 iterationCapHits
the loop hit maxIter without leaving the window
long long unstickPushes
a stalled step that had to be nudged to continue
double insideLength
summed inside-segment length, cm (the chord integral)
long long nonAdvancingSteps
the accumulated distance did not increase
double zeroStep
A step at or below this is a stall, not progress.
#define main
std::unique_ptr< TTree > tree((TTree *) flIn.Get(std::string(o2::base::NameConf::CTFTREENAME).c_str()))