Project
Loading...
Searching...
No Matches
runSolidHarness.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
22
27
28#include "TGeoBBox.h"
29#include "TGeoCompositeShape.h"
30#include "TGeoMatrix.h"
31#include "TGeoScaledShape.h"
32
33#include <nlohmann/json.hpp>
34
35#include <algorithm>
36#include <array>
37#include <cctype>
38#include <chrono>
39#include <cstdio>
40#include <cmath>
41#include <cstring>
42#include <fstream>
43#include <map>
44#include <memory>
45#include <iostream>
46#include <optional>
47#include <set>
48#include <sstream>
49#include <string>
50#include <vector>
51
52using json = nlohmann::json;
53using namespace o2::cad;
54using namespace o2::cad::harness;
56
57namespace
58{
59
60struct Options {
61 std::string db;
62 std::string explicitSurfaces;
63 std::string explicitFacets;
64 std::string partsPattern;
65 int points = 5000;
66 int rays = 5000;
67 uint64_t seed = 1;
68 std::set<std::string> only = {"contains", "distout", "distin", "safety"};
69 bool loopCrosscheck = false;
70 bool pruningAb = false;
71 bool allRims = false;
72 std::string jsonOut;
73 int warmup = 1;
74 int repeat = 3;
75 std::string dumpSamples;
76 std::string refAnswers;
77 std::string loadSamples;
78 bool edgeIdentity = false;
79 std::string explicitShape;
80};
81
82struct Part {
83 std::string id;
84 std::string model;
85 std::string surfaces;
86 std::string facets;
89 std::string shape;
90};
91
98std::string deriveShapeSidecarPath(const std::string& surfacesPath)
99{
100 const auto slash = surfacesPath.find_last_of('/');
101 const std::string dir = slash == std::string::npos ? std::string() : surfacesPath.substr(0, slash + 1);
102 std::string base = slash == std::string::npos ? surfacesPath : surfacesPath.substr(slash + 1);
103 const std::string prefix = "surfaces_";
104 const std::string suffix = ".bin";
105 if (base.rfind(prefix, 0) != 0 || base.size() <= prefix.size() + suffix.size() ||
106 base.compare(base.size() - suffix.size(), suffix.size(), suffix) != 0) {
107 return {};
108 }
109 const std::string stem = base.substr(prefix.size(), base.size() - prefix.size() - suffix.size());
110 return dir + "shape_" + stem + ".root";
111}
112
113bool fileExists(const std::string& path)
114{
115 if (path.empty()) {
116 return false;
117 }
118 std::ifstream probe(path);
119 return static_cast<bool>(probe);
120}
121
122void printUsage(const char* argv0)
123{
124 std::cout << "Usage: " << argv0 << " --db <dir> [--parts <substring>] [--points N] [--rays N] [--seed N]\n"
125 " [--only contains,distout,distin,safety] [--loop-crosscheck]\n"
126 " [--pruning-ab] [--json <out.json>] [--warmup N] [--repeat N]\n"
127 " or: "
128 << argv0 << " --surfaces <file> --facets <file> [--shape <file>] [options as above]\n\n"
129 " Every representation a part has is scored side by side against the same oracle answers:\n"
130 " surface surfaces_<part>.bin -> O2BVHSurfaceSolid (the historical candidate)\n"
131 " mesh facets_<part>.bin -> O2Tessellated (also the sampling reference)\n"
132 " shape shape_<part>.root -> any TGeoShape (the CSG emitter's hand-over)\n"
133 " The `shape` sidecar is one ROOT file holding one TGeoShape-derived object under the key\n"
134 " \"shape\", in cm, plus an OPTIONAL TGeoHMatrix under the key \"placement\" taking it from\n"
135 " its own frame into the part's; absent means identity, and points and rays are transformed\n"
136 " into the shape's frame before it is asked. See CADSupport/O2SolidHarness.h.\n\n"
137 " --loop-crosscheck also run the surface solid's non-BVH _Loop twins and require exact\n"
138 " agreement; this is the correctness guard that does not involve the mesh\n"
139 " --pruning-ab re-run the distance kernels with ray tmax pruning disabled, reporting\n"
140 " the BVH candidate counts and ns/call both ways (prices the optimization)\n"
141 " --rims list every trim loop, not only the ones that are not cleanly matched;\n"
142 " the same records go into --json unconditionally\n"
143 " --dump-samples D write each part's sample set to D/samples_<part>.json\n"
144 " --load-samples D read each part's sample set from D/samples_<part>.json instead of\n"
145 " generating it. The generator derives its points from the *mesh*, so two\n"
146 " runs on differently-tessellated shapes cannot be compared point by\n"
147 " point; loading a frozen (and, for a transformed shape, transformed) set\n"
148 " removes the mesh from the comparison entirely. --points/--rays/--seed\n"
149 " are then ignored and the file's counts are used.\n"
150 " --edge-identity report the sidecar-v3 edge-identity block (source-edge counts and the\n"
151 " max shared-edge deviation) on stdout; it is always in --json\n"
152 " --ref-answers D validate against D/answers_<part>.json instead of the mesh; those are\n"
153 " produced by Detectors/CADSupport/validation/occtOracle.py from the part's .brep, so a\n"
154 " disagreement outside the model tolerance is a defect, not chording\n\n"
155 "OCCT oracle round trip:\n"
156 " "
157 << argv0 << " --db <db> --dump-samples /tmp/o\n"
158 " occtOracle.py --brep <part>.brep --samples /tmp/o/samples_<part>.json \\\n"
159 " --out /tmp/o/answers_<part>.json\n"
160 " "
161 << argv0 << " --db <db> --ref-answers /tmp/o\n\n"
162 "perf record entry point (single kernel, one part):\n"
163 " perf record -g "
164 << argv0 << " --db <db> --parts ExcavatorArm --only distout --rays 200000\n";
165}
166
167std::set<std::string> splitCsv(const std::string& s)
168{
169 std::set<std::string> out;
170 std::stringstream ss(s);
171 std::string tok;
172 while (std::getline(ss, tok, ',')) {
173 if (!tok.empty()) {
174 out.insert(tok);
175 }
176 }
177 return out;
178}
179
180bool parseArgs(int argc, char** argv, Options& opt)
181{
182 for (int i = 1; i < argc; ++i) {
183 const std::string a = argv[i];
184 auto next = [&](const char* flag) -> std::string {
185 if (i + 1 >= argc) {
186 throw std::runtime_error(std::string("missing value for ") + flag);
187 }
188 return argv[++i];
189 };
190 if (a == "--db") {
191 opt.db = next("--db");
192 } else if (a == "--surfaces") {
193 opt.explicitSurfaces = next("--surfaces");
194 } else if (a == "--facets") {
195 opt.explicitFacets = next("--facets");
196 } else if (a == "--shape") {
197 opt.explicitShape = next("--shape");
198 } else if (a == "--parts") {
199 opt.partsPattern = next("--parts");
200 } else if (a == "--points") {
201 opt.points = std::stoi(next("--points"));
202 } else if (a == "--rays") {
203 opt.rays = std::stoi(next("--rays"));
204 } else if (a == "--seed") {
205 opt.seed = std::stoull(next("--seed"));
206 } else if (a == "--only") {
207 opt.only = splitCsv(next("--only"));
208 } else if (a == "--loop-crosscheck") {
209 opt.loopCrosscheck = true;
210 } else if (a == "--pruning-ab") {
211 opt.pruningAb = true;
212 } else if (a == "--rims") {
213 opt.allRims = true;
214 } else if (a == "--json") {
215 opt.jsonOut = next("--json");
216 } else if (a == "--warmup") {
217 opt.warmup = std::stoi(next("--warmup"));
218 } else if (a == "--repeat") {
219 opt.repeat = std::stoi(next("--repeat"));
220 } else if (a == "--dump-samples") {
221 opt.dumpSamples = next("--dump-samples");
222 } else if (a == "--ref-answers") {
223 opt.refAnswers = next("--ref-answers");
224 } else if (a == "--load-samples") {
225 opt.loadSamples = next("--load-samples");
226 } else if (a == "--edge-identity") {
227 opt.edgeIdentity = true;
228 } else if (a == "-h" || a == "--help") {
229 printUsage(argv[0]);
230 return false;
231 } else {
232 throw std::runtime_error("unrecognized option: " + a);
233 }
234 }
235 if (opt.db.empty() && (opt.explicitSurfaces.empty() || opt.explicitFacets.empty())) {
236 throw std::runtime_error("either --db <dir> or both --surfaces/--facets are required");
237 }
238 return true;
239}
240
241std::vector<Part> collectParts(const Options& opt)
242{
243 std::vector<Part> parts;
244 if (!opt.explicitSurfaces.empty()) {
245 Part part{"adhoc", "adhoc", opt.explicitSurfaces, opt.explicitFacets, opt.explicitShape};
246 if (part.shape.empty()) {
247 part.shape = deriveShapeSidecarPath(part.surfaces);
248 }
249 parts.push_back(std::move(part));
250 return parts;
251 }
252 const std::string manifestPath = opt.db + "/manifest.json";
253 std::ifstream in(manifestPath);
254 if (!in) {
255 throw std::runtime_error("cannot open " + manifestPath);
256 }
257 json manifest;
258 in >> manifest;
259 for (const auto& p : manifest.at("parts")) {
260 Part part;
261 part.id = p.at("id").get<std::string>();
262 part.model = p.at("model").get<std::string>();
263 part.surfaces = p.at("surfaces").get<std::string>();
264 part.facets = p.at("facets").get<std::string>();
265 part.shape = p.value("shape", std::string());
266 if (part.shape.empty()) {
267 part.shape = deriveShapeSidecarPath(part.surfaces);
268 }
269 if (!opt.partsPattern.empty()) {
270 const bool idMatch = part.id.find(opt.partsPattern) != std::string::npos;
271 const bool modelMatch = part.model.find(opt.partsPattern) != std::string::npos;
272 if (!idMatch && !modelMatch) {
273 continue;
274 }
275 }
276 parts.push_back(std::move(part));
277 }
278 return parts;
279}
280
281json validationToJson(const ValidationResult& r)
282{
283 json j;
284 j["nSamples"] = r.nSamples;
285 j["nAgree"] = r.nAgree;
286 j["nMismatchWithinBand"] = r.nMismatchWithinBand;
287 j["nMismatchMissedSurface"] = r.nMismatchMissedSurface;
288 j["nMismatchUnexplained"] = r.nMismatchUnexplained;
289 j["nNoVerdict"] = r.nNoVerdict;
290 j["nRelabelled"] = r.nRelabelled;
291 j["worstDeviation"] = r.worstDeviation;
292 json offenders = json::array();
293 for (const auto& o : r.worstOffenders) {
294 offenders.push_back({{"point", {o.point[0], o.point[1], o.point[2]}},
295 {"dir", {o.dir[0], o.dir[1], o.dir[2]}},
296 {"candidateValue", o.candidateValue},
297 {"referenceValue", o.referenceValue},
298 {"deviation", o.deviation},
299 {"referenceSafety", o.referenceSafety},
300 {"incidenceCosine", o.incidenceCosine}});
301 }
302 j["worstOffenders"] = offenders;
303 return j;
304}
305
306// The sample/answer JSON contract shared with Detectors/CADSupport/validation/occtOracle.py. Bump on both sides
307// together; the oracle refuses a version it does not speak rather than guessing.
308constexpr int kOracleFormatVersion = 1;
309
312std::string sanitizePartId(const std::string& id)
313{
314 std::string out;
315 out.reserve(id.size());
316 for (const char c : id) {
317 out.push_back((std::isalnum(static_cast<unsigned char>(c)) || c == '-' || c == '.') ? c : '_');
318 }
319 return out;
320}
321
322json pointsToJson(const std::vector<Point3D>& points)
323{
324 json array = json::array();
325 for (const auto& p : points) {
326 array.push_back({p[0], p[1], p[2]});
327 }
328 return array;
329}
330
331json raysToJson(const std::vector<Ray>& rays)
332{
333 json array = json::array();
334 for (const auto& r : rays) {
335 array.push_back({{"o", {r.origin[0], r.origin[1], r.origin[2]}},
336 {"d", {r.dir[0], r.dir[1], r.dir[2]}}});
337 }
338 return array;
339}
340
344void writeSamples(const std::string& dir, const std::string& partId, const SampleSet& samples)
345{
346 json doc;
347 doc["version"] = kOracleFormatVersion;
348 doc["part"] = partId;
349 doc["bboxMin"] = {samples.bboxMin[0], samples.bboxMin[1], samples.bboxMin[2]};
350 doc["bboxMax"] = {samples.bboxMax[0], samples.bboxMax[1], samples.bboxMax[2]};
351 doc["points"] = {{"bulk", pointsToJson(samples.bulkPoints)},
352 {"boundary", pointsToJson(samples.boundaryPoints)},
353 {"inside", pointsToJson(samples.insidePoints)}};
354 doc["rays"] = {{"outside", raysToJson(samples.outsideRays)},
355 {"inside", raysToJson(samples.insideRays)}};
356 const std::string path = dir + "/samples_" + sanitizePartId(partId) + ".json";
357 std::ofstream out(path);
358 if (!out) {
359 throw std::runtime_error("cannot write " + path);
360 }
361 out << doc.dump(1);
362 std::printf(" wrote samples: %s\n", path.c_str());
363}
364
365std::vector<Point3D> pointsFromJson(const json& array)
366{
367 std::vector<Point3D> points;
368 points.reserve(array.size());
369 for (const auto& p : array) {
370 points.push_back(Point3D{p.at(0).get<double>(), p.at(1).get<double>(), p.at(2).get<double>()});
371 }
372 return points;
373}
374
375std::vector<Ray> raysFromJson(const json& array)
376{
377 std::vector<Ray> rays;
378 rays.reserve(array.size());
379 for (const auto& r : array) {
380 const auto& o = r.at("o");
381 const auto& d = r.at("d");
382 rays.push_back(Ray{Point3D{o.at(0).get<double>(), o.at(1).get<double>(), o.at(2).get<double>()},
383 Point3D{d.at(0).get<double>(), d.at(1).get<double>(), d.at(2).get<double>()}});
384 }
385 return rays;
386}
387
390SampleSet readSamples(const std::string& dir, const std::string& partId)
391{
392 const std::string path = dir + "/samples_" + sanitizePartId(partId) + ".json";
393 std::ifstream in(path);
394 if (!in) {
395 throw std::runtime_error("cannot read " + path);
396 }
397 json doc;
398 in >> doc;
399 const int version = doc.value("version", -1);
400 if (version != kOracleFormatVersion) {
401 throw std::runtime_error(path + ": sample format version " + std::to_string(version) +
402 ", this harness speaks " + std::to_string(kOracleFormatVersion));
403 }
405 for (int i = 0; i < 3; ++i) {
406 samples.bboxMin[i] = doc.at("bboxMin").at(i).get<double>();
407 samples.bboxMax[i] = doc.at("bboxMax").at(i).get<double>();
408 }
409 samples.bulkPoints = pointsFromJson(doc.at("points").at("bulk"));
410 samples.boundaryPoints = pointsFromJson(doc.at("points").at("boundary"));
411 samples.insidePoints = pointsFromJson(doc.at("points").at("inside"));
412 samples.outsideRays = raysFromJson(doc.at("rays").at("outside"));
413 samples.insideRays = raysFromJson(doc.at("rays").at("inside"));
414 std::printf(" loaded samples: %s (bulk=%zu boundary=%zu inside=%zu outRays=%zu inRays=%zu)\n",
415 path.c_str(), samples.bulkPoints.size(), samples.boundaryPoints.size(),
416 samples.insidePoints.size(), samples.outsideRays.size(), samples.insideRays.size());
417 return samples;
418}
419
421struct OracleAnswers {
422 bool has = false;
423 double tolerance = 0.;
424 double capacity = 0.;
425 bool valid = false;
428 bool hasBbox = false;
429 Point3D bboxMin{};
430 Point3D bboxMax{};
431 std::map<std::string, std::vector<int>> containsState;
434 std::map<std::string, std::vector<int>> originContains;
435 std::map<std::string, std::vector<double>> boundaryDistance;
436 std::map<std::string, std::vector<double>> distOutside;
437 std::map<std::string, std::vector<double>> distInside;
438};
439
440template <typename T>
441std::map<std::string, std::vector<T>> readColumns(const json& parent, const char* key)
442{
443 std::map<std::string, std::vector<T>> columns;
444 if (!parent.contains(key)) {
445 return columns;
446 }
447 for (const auto& [category, values] : parent.at(key).items()) {
448 columns[category] = values.template get<std::vector<T>>();
449 }
450 return columns;
451}
452
453OracleAnswers loadOracleAnswers(const std::string& dir, const std::string& partId)
454{
455 OracleAnswers answers;
456 const std::string path = dir + "/answers_" + sanitizePartId(partId) + ".json";
457 std::ifstream in(path);
458 if (!in) {
459 std::printf(" oracle: no answers file %s, skipping oracle validation\n", path.c_str());
460 return answers;
461 }
462 json doc;
463 in >> doc;
464 const int version = doc.value("version", -1);
465 if (version != kOracleFormatVersion) {
466 throw std::runtime_error(path + ": answer format version " + std::to_string(version) +
467 ", this harness speaks " + std::to_string(kOracleFormatVersion));
468 }
469 answers.has = true;
470 answers.tolerance = doc.value("tolerance", 0.);
471 answers.capacity = doc.value("capacity", 0.);
472 answers.valid = doc.value("valid", false);
473 if (doc.contains("bboxMin") && doc.contains("bboxMax")) {
474 answers.hasBbox = true;
475 for (int i = 0; i < 3; ++i) {
476 answers.bboxMin[i] = doc.at("bboxMin").at(i).get<double>();
477 answers.bboxMax[i] = doc.at("bboxMax").at(i).get<double>();
478 }
479 }
480 answers.containsState = readColumns<int>(doc, "contains");
481 answers.originContains = readColumns<int>(doc, "originContains");
482 answers.boundaryDistance = readColumns<double>(doc, "safetyUpperBound");
483 answers.distOutside = readColumns<double>(doc, "distFromOutside");
484 answers.distInside = readColumns<double>(doc, "distFromInside");
485 return answers;
486}
487
496template <typename T>
497std::vector<T> mergeCategories(const std::map<std::string, std::vector<T>>& columns,
498 const std::array<size_t, 3>& categorySizes, T missing)
499{
500 static constexpr std::array<const char*, 3> kOrder = {"bulk", "boundary", "inside"};
501 std::vector<T> merged;
502 for (size_t categoryIndex = 0; categoryIndex < kOrder.size(); ++categoryIndex) {
503 const size_t expected = categorySizes[categoryIndex];
504 const auto it = columns.find(kOrder[categoryIndex]);
505 const size_t available = it == columns.end() ? 0 : std::min(expected, it->second.size());
506 for (size_t i = 0; i < available; ++i) {
507 merged.push_back(it->second[i]);
508 }
509 merged.insert(merged.end(), expected - available, missing);
510 }
511 return merged;
512}
513
514json timingToJson(const TimingResult& t)
515{
516 return {{"nCalls", t.nCalls}, {"nsPerCall", t.nsPerCall}, {"checksum", t.checksum}};
517}
518
519void printValidation(const std::string& name, const ValidationResult& r)
520{
521 // Scored = everything the reference was willing to answer. Reporting the percentage against
522 // nSamples would let a reference that abstains on half the points look like agreement.
523 const size_t scored = r.nSamples - r.nNoVerdict;
524 const double agreePct = scored ? 100. * static_cast<double>(r.nAgree) / static_cast<double>(scored) : 0.;
525 std::printf(
526 " %-10s scored=%-7zu agree=%6.2f%% mismatch(band=%zu,missed=%zu,unexplained=%zu)"
527 " noVerdict=%zu worstDev=%.6g\n",
528 name.c_str(), scored, agreePct, r.nMismatchWithinBand, r.nMismatchMissedSurface,
529 r.nMismatchUnexplained, r.nNoVerdict, r.worstDeviation);
530 if (r.nRelabelled > 0) {
531 // Not a candidate result: it says how many rays the sample generator had put in the wrong
532 // category, which is a statement about the reference mesh. Printed so an improvement in these
533 // columns is never mistaken for a kernel improvement.
534 std::printf(" %-10s relabelled=%zu ray(s) by the oracle's own origin classification\n",
535 name.c_str(), r.nRelabelled);
536 }
537 if (r.nMismatchUnexplained > 0 || r.nMismatchMissedSurface > 0) {
538 const size_t nShow = std::min<size_t>(3, r.worstOffenders.size());
539 for (size_t i = 0; i < nShow; ++i) {
540 const auto& o = r.worstOffenders[i];
541 std::printf(" offender[%zu]: point=(%.6g,%.6g,%.6g) dir=(%.6g,%.6g,%.6g) cand=%.6g ref=%.6g dev=%.6g refSafety=%.6g\n",
542 i, o.point[0], o.point[1], o.point[2], o.dir[0], o.dir[1], o.dir[2], o.candidateValue,
543 o.referenceValue, o.deviation, o.referenceSafety);
544 }
545 }
546}
547
548void printTiming(const std::string& name, const TimingResult& candidate, const TimingResult& reference)
549{
550 const double ratio = reference.nsPerCall > 0. ? candidate.nsPerCall / reference.nsPerCall : 0.;
551 std::printf(" %-10s candidate=%9.1f ns/call reference=%9.1f ns/call ratio(cand/ref)=%.2fx\n", name.c_str(),
552 candidate.nsPerCall, reference.nsPerCall, ratio);
553}
554
555// What the BVH traversal buys over the all-surfaces loop on the *same* shape: unlike the
556// candidate/reference ratio this compares like with like, so it prices the acceleration structure
557// alone rather than analytic patches against triangles.
558void printLoopSpeedup(const std::string& name, const TimingResult& bvh, const TimingResult& loop)
559{
560 const double speedup = bvh.nsPerCall > 0. ? loop.nsPerCall / bvh.nsPerCall : 0.;
561 std::printf(" %-10s BVH=%9.1f ns/call _Loop=%9.1f ns/call speedup(loop/bvh)=%.2fx\n", name.c_str(),
562 bvh.nsPerCall, loop.nsPerCall, speedup);
563}
564
565double toSeconds(std::chrono::steady_clock::time_point t0, std::chrono::steady_clock::time_point t1)
566{
567 return std::chrono::duration<double>(t1 - t0).count();
568}
569
570// ------------------------------------------------------------------------------------------
571// Representations: the same part, scored several ways against one set of oracle answers
572// ------------------------------------------------------------------------------------------
573//
574// The four scored queries are TGeoShape virtuals, so the scoring loop below has no business
575// knowing what it is scoring. Everything that is specific to O2BVHSurfaceSolid -- closure, rims,
576// NavigationReliability, the _Loop twins, the BVH candidate counters -- hangs off `surfaceSolid`,
577// which is null for every other representation, and is reported only where it means something.
578// A TGeoCompositeShape has no rims and no closure; reporting "reliable" or "not navigable" for it
579// would be a category error, so those keys are simply absent from its entry and a
580// `closureApplicable: false` says why.
581
582struct Representation {
583 std::string name;
584 std::string source;
585 const TGeoShape* shape = nullptr;
586 const O2BVHSurfaceSolid* surfaceSolid = nullptr;
587 int primitives = 0;
588 const char* primitiveKind = "";
599 const TGeoMatrix* placement = nullptr;
600};
601
603Point3D toLocal(const TGeoMatrix* placement, const Point3D& p)
604{
605 if (placement == nullptr) {
606 return p;
607 }
608 Point3D out{};
609 placement->MasterToLocal(p.data(), out.data());
610 return out;
611}
612
616Ray toLocal(const TGeoMatrix* placement, const Ray& r)
617{
618 if (placement == nullptr) {
619 return r;
620 }
621 Ray out{};
622 placement->MasterToLocal(r.origin.data(), out.origin.data());
623 placement->MasterToLocalVect(r.dir.data(), out.dir.data());
624 return out;
625}
626
630template <typename T>
631std::vector<T> toLocal(const TGeoMatrix* placement, const std::vector<T>& in)
632{
633 if (placement == nullptr) {
634 return {};
635 }
636 std::vector<T> out;
637 out.reserve(in.size());
638 for (const auto& item : in) {
639 out.push_back(toLocal(placement, item));
640 }
641 return out;
642}
643
653struct CapacityKind {
654 const char* method = "root-analytic";
655 bool comparable = true;
656};
657
658bool usesMonteCarloCapacity(const TGeoShape* shape)
659{
660 if (shape == nullptr) {
661 return false;
662 }
663 if (shape->InheritsFrom(TGeoCompositeShape::Class())) {
664 return true;
665 }
666 // TGeoScaledShape::Capacity() forwards to the shape it wraps, so a scaled composite is just as
667 // sampled as a bare one.
668 if (const auto* scaled = dynamic_cast<const TGeoScaledShape*>(shape)) {
669 return usesMonteCarloCapacity(scaled->GetShape());
670 }
671 return false;
672}
673
674CapacityKind capacityKindOf(const Representation& rep)
675{
676 if (rep.surfaceSolid != nullptr) {
677 // Divergence theorem in closed form over the analytic faces.
678 return {"exact-divergence", true};
679 }
680 if (dynamic_cast<const O2Tessellated*>(rep.shape) != nullptr) {
681 // Exact for the mesh (signed tetrahedra over its own triangles), deterministic, and therefore
682 // a real measurement -- of the chording deficit, not of a bug.
683 return {"mesh-divergence", true};
684 }
685 if (usesMonteCarloCapacity(rep.shape)) {
686 return {"root-montecarlo", false};
687 }
688 return {"root-analytic", true};
689}
690
705double bboxDeviationFromOracle(const TGeoShape* shape, const OracleAnswers& oracle,
706 const TGeoMatrix* placement = nullptr)
707{
708 if (!oracle.hasBbox) {
709 return -1.;
710 }
711 const auto* box = dynamic_cast<const TGeoBBox*>(shape);
712 if (box == nullptr) {
713 return -1.;
714 }
715 const double half[3] = {box->GetDX(), box->GetDY(), box->GetDZ()};
716 double lo[3];
717 double hi[3];
718 for (int i = 0; i < 3; ++i) {
719 lo[i] = box->GetOrigin()[i] - half[i];
720 hi[i] = box->GetOrigin()[i] + half[i];
721 }
722 if (placement != nullptr) {
723 double outLo[3] = {1.e300, 1.e300, 1.e300};
724 double outHi[3] = {-1.e300, -1.e300, -1.e300};
725 for (int corner = 0; corner < 8; ++corner) {
726 const double local[3] = {(corner & 1) ? hi[0] : lo[0], (corner & 2) ? hi[1] : lo[1],
727 (corner & 4) ? hi[2] : lo[2]};
728 double master[3];
729 placement->LocalToMaster(local, master);
730 for (int i = 0; i < 3; ++i) {
731 outLo[i] = std::min(outLo[i], master[i]);
732 outHi[i] = std::max(outHi[i], master[i]);
733 }
734 }
735 std::copy(std::begin(outLo), std::end(outLo), std::begin(lo));
736 std::copy(std::begin(outHi), std::end(outHi), std::begin(hi));
737 }
738 double worst = 0.;
739 for (int i = 0; i < 3; ++i) {
740 worst = std::max(worst, std::fabs(lo[i] - oracle.bboxMin[i]));
741 worst = std::max(worst, std::fabs(hi[i] - oracle.bboxMax[i]));
742 }
743 return worst;
744}
745
752json scoreAgainstOracle(const TGeoShape* candidate, const OracleAnswers& oracle,
753 const ValidationOptions& oracleOpt, const std::vector<Point3D>& allPointsIn,
754 const std::vector<int>& containsState,
755 const std::vector<double>& boundaryDistance, const SampleSet& samplesIn,
756 const std::set<std::string>& only, const std::string& label,
757 const std::string& capacityLabel, const TGeoMatrix* placement = nullptr)
758{
759 // The samples are stated in the part frame -- the frame the oracle answered in. A shape that
760 // carries a placement answers in its own, so the *questions* move and the answers do not: a
761 // rigid transform preserves both the inside/outside relation and every distance along a ray.
762 const std::vector<Point3D> localPoints = toLocal(placement, allPointsIn);
763 const std::vector<Ray> localOutsideRays = toLocal(placement, samplesIn.outsideRays);
764 const std::vector<Ray> localInsideRays = toLocal(placement, samplesIn.insideRays);
765 const std::vector<Point3D>& allPoints = placement != nullptr ? localPoints : allPointsIn;
766 const std::vector<Ray>& outsideRays =
767 placement != nullptr ? localOutsideRays : samplesIn.outsideRays;
768 const std::vector<Ray>& insideRays = placement != nullptr ? localInsideRays : samplesIn.insideRays;
769 json oracleJson;
770 oracleJson["tolerance"] = oracle.tolerance;
771 oracleJson["capacity"] = oracle.capacity;
772 oracleJson["valid"] = oracle.valid;
773 const double capacity = candidate->Capacity();
774 oracleJson["capacityCandidate"] = capacity;
775 oracleJson["capacityRelativeDeviation"] =
776 oracle.capacity != 0. ? (capacity - oracle.capacity) / oracle.capacity : 0.;
777 std::printf(" %s: capacity candidate=%.6g reference=%.6g relDev=%.3g\n", capacityLabel.c_str(),
778 capacity, oracle.capacity, oracleJson["capacityRelativeDeviation"].get<double>());
779
780 if (only.count("contains")) {
781 auto v = validateContainsAgainstOracle(candidate, allPoints, containsState, boundaryDistance,
782 oracleOpt);
783 printValidation(label + ":contains", v);
784 oracleJson["contains"] = validationToJson(v);
785 }
786 const auto originStateFor = [&oracle](const char* category) {
787 const auto it = oracle.originContains.find(category);
788 return it == oracle.originContains.end() ? std::vector<int>{} : it->second;
789 };
790 if (only.count("distout")) {
791 const auto it = oracle.distOutside.find("outside");
792 if (it != oracle.distOutside.end()) {
793 auto v = validateDistanceAgainstOracle(candidate, outsideRays, it->second,
794 /*wantInside=*/false, oracleOpt,
795 originStateFor("outside"));
796 printValidation(label + ":distout", v);
797 oracleJson["distout"] = validationToJson(v);
798 }
799 }
800 if (only.count("distin")) {
801 const auto it = oracle.distInside.find("inside");
802 if (it != oracle.distInside.end()) {
803 auto v = validateDistanceAgainstOracle(candidate, insideRays, it->second,
804 /*wantInside=*/true, oracleOpt, originStateFor("inside"));
805 printValidation(label + ":distin", v);
806 oracleJson["distin"] = validationToJson(v);
807 }
808 }
809 if (only.count("safety")) {
810 auto v = validateSafetyAgainstOracle(candidate, allPoints, boundaryDistance, oracleOpt);
811 printValidation(label + ":safety", v);
812 oracleJson["safety"] = validationToJson(v);
813 }
814 return oracleJson;
815}
816
820size_t countDisagreements(const json& oracleJson)
821{
822 size_t bad = 0;
823 for (const char* key : {"contains", "distout", "distin", "safety"}) {
824 if (!oracleJson.contains(key)) {
825 continue;
826 }
827 const auto& column = oracleJson.at(key);
828 bad += column.value("nMismatchUnexplained", size_t{0});
829 bad += column.value("nMismatchMissedSurface", size_t{0});
830 }
831 return bad;
832}
833
834} // namespace
835
836int main(int argc, char** argv)
837{
838 Options opt;
839 try {
840 if (!parseArgs(argc, argv, opt)) {
841 return 0;
842 }
843 } catch (const std::exception& e) {
844 std::cerr << "error: " << e.what() << "\n";
845 printUsage(argv[0]);
846 return 1;
847 }
848
849 std::vector<Part> parts;
850 try {
851 parts = collectParts(opt);
852 } catch (const std::exception& e) {
853 std::cerr << "error: " << e.what() << "\n";
854 return 1;
855 }
856 if (parts.empty()) {
857 std::cerr << "no parts matched (pattern='" << opt.partsPattern << "')\n";
858 return 1;
859 }
860
861 json jsonReport = json::array();
862 std::vector<std::string> unreliableParts;
863
864 for (const auto& part : parts) {
865 std::printf("=== %s (%s) ===\n", part.id.c_str(), part.model.c_str());
866
867 O2BVHSurfaceSolid surf(part.id.c_str());
868 if (!LoadSurfaceSolid(part.surfaces, surf)) {
869 std::cerr << " skip: LoadSurfaceSolid failed for " << part.surfaces << "\n";
870 continue;
871 }
872 auto t0 = std::chrono::steady_clock::now();
873 surf.CloseShape(true);
874 auto t1 = std::chrono::steady_clock::now();
875 const double surfCloseSeconds = toSeconds(t0, t1);
876
877 O2Tessellated mesh(part.id.c_str());
878 if (!LoadFacetSolid(part.facets, mesh)) {
879 std::cerr << " skip: LoadFacetSolid failed for " << part.facets << "\n";
880 continue;
881 }
882 t0 = std::chrono::steady_clock::now();
883 mesh.CloseShape();
884 t1 = std::chrono::steady_clock::now();
885 const double meshCloseSeconds = toSeconds(t0, t1);
886
887 const TGeoShape* candidate = &surf;
888 const TGeoShape* reference = &mesh;
889
890 // Every representation this part has, in the order they are reported. `surface` first so the
891 // historical candidate keeps its place; `mesh` second because it is also the sampling
892 // reference; `shape` last because it is optional and does not exist yet for any converted
893 // part -- it is the slot the CSG emitter writes into.
894 std::vector<Representation> representations;
895 representations.push_back({"surface", part.surfaces, &surf, &surf, surf.GetNsurfaces(), "patches"});
896 representations.push_back({"mesh", part.facets, &mesh, nullptr, mesh.GetNfacets(), "triangles"});
897 std::unique_ptr<TGeoShape> rootShape;
898 std::unique_ptr<TGeoHMatrix> rootShapePlacement;
899 if (fileExists(part.shape)) {
900 std::string shapeError;
901 rootShape.reset(loadShapeFromRootFile(part.shape, &shapeError));
902 if (rootShape) {
903 rootShapePlacement.reset(loadShapePlacementFromRootFile(part.shape));
904 std::printf(" shape sidecar: %s -> %s \"%s\"%s\n", part.shape.c_str(),
905 rootShape->ClassName(), rootShape->GetName(),
906 rootShapePlacement ? " (placed: queries are transformed into its own frame)"
907 : "");
908 representations.push_back({"shape", part.shape, rootShape.get(), nullptr, -1,
909 rootShape->ClassName(), rootShapePlacement.get()});
910 } else {
911 std::printf(" shape sidecar: *** %s\n", shapeError.c_str());
912 }
913 }
914
915 const Point3D bboxMin{mesh.GetOrigin()[0] - mesh.GetDX(), mesh.GetOrigin()[1] - mesh.GetDY(),
916 mesh.GetOrigin()[2] - mesh.GetDZ()};
917 const Point3D bboxMax{mesh.GetOrigin()[0] + mesh.GetDX(), mesh.GetOrigin()[1] + mesh.GetDY(),
918 mesh.GetOrigin()[2] + mesh.GetDZ()};
919
920 std::printf(" surfaces=%d triangles=%d closeShape: surface=%.4fs mesh=%.4fs\n", surf.GetNsurfaces(),
921 mesh.GetNfacets(), surfCloseSeconds, meshCloseSeconds);
922
923 // Label every measurement with whether its subject is a closed manifold at all.
924 const auto reliability = surf.GetNavigationReliability();
925 const char* reliabilityName = O2BVHSurfaceSolid::GetNavigationReliabilityName(reliability);
926 const bool navigable = surf.IsNavigable();
927 std::printf(" navigation: %s%s (boundary=%d non-manifold=%d reversed=%d)\n", reliabilityName,
928 navigable ? "" : " *** UNRELIABLE: results below are not a measurement of accuracy ***",
929 surf.GetBoundaryEdgeCount(), surf.GetNonManifoldEdgeCount(), surf.GetReversedEdgeCount());
930 // The same boundary measured as curves, in cm. The isolation is how alone the loneliest chord
931 // is, *not* a seam width; the chord resolution is next to it because it is what widens the
932 // band each chord is matched in, over the declared tolerance.
933 std::printf(
934 " rim isolation: max %.3g cm (chord resolution %.3g cm, declared tolerance %.3g cm); rims %d "
935 "(matched=%d boundary=%d non-manifold=%d reversed=%d), open %.3g of %.3g cm\n",
936 surf.GetMaxRimIsolation(), surf.GetRimChordResolution(), surf.GetRimMatchTolerance(), surf.GetRimCount(),
937 surf.GetMatchedRimCount(), surf.GetBoundaryRimCount(), surf.GetNonManifoldRimCount(),
938 surf.GetReversedRimCount(), surf.GetUnmatchedRimLength(), surf.GetTotalRimLength());
939 // Sidecar v3: closure decided by edge *identity* rather than by proximity. The
940 // deviation is a measured cm number and deliberately not a verdict -- it says how far the two
941 // faces that provably share an edge actually are, which is the first defensible answer this
942 // project has had to that question. Always in --json; on stdout only when asked, because a
943 // 19-part run is already dense.
944 if (opt.edgeIdentity) {
945 if (surf.HasEdgeIdentity()) {
946 std::printf(
947 " edge identity: %d source edge(s) (shared=%d boundary=%d non-manifold=%d "
948 "reversed=%d degenerate=%d), max shared-edge deviation %.4g cm\n",
949 surf.GetSourceEdgeCount(), surf.GetSharedSourceEdgeCount(),
950 surf.GetBoundarySourceEdgeCount(), surf.GetNonManifoldSourceEdgeCount(),
951 surf.GetReversedSourceEdgeCount(), surf.GetDegenerateSourceEdgeCount(),
952 surf.GetMaxSharedEdgeDeviation());
953 } else {
954 std::printf(" edge identity: absent (sidecar predates v3); closure fell back to proximity\n");
955 }
956 }
957 // Name the offending rims; the line above gives only their count and length.
958 json rimsJson = json::array();
959 for (const auto& rim : surf.GetRimReports()) {
960 const char* stateName = O2BVHSurfaceSolid::GetNavigationReliabilityName(rim.state);
961 const bool clean = rim.state == O2BVHSurfaceSolid::NavigationReliability::Reliable;
962 if (opt.allRims || !clean) {
963 std::printf(
964 " rim face=%d loop=%d %s %s: %d chords, %.4g cm (%d chords / %.4g cm unmatched); "
965 "loneliest chord %.3g cm from face %d at (%.4g, %.4g, %.4g)\n",
966 rim.surface, rim.rimOnSurface, rim.closed ? "closed" : "OPEN-CHAIN", stateName, rim.chords,
967 rim.length, rim.unmatchedChords, rim.unmatchedLength, rim.maxIsolation, rim.maxIsolationFace,
968 rim.maxIsolationPoint[0], rim.maxIsolationPoint[1], rim.maxIsolationPoint[2]);
969 }
970 rimsJson.push_back({{"face", rim.surface},
971 {"loop", rim.rimOnSurface},
972 {"state", stateName},
973 {"closed", rim.closed},
974 {"chords", rim.chords},
975 {"unmatchedChords", rim.unmatchedChords},
976 {"length", rim.length},
977 {"unmatchedLength", rim.unmatchedLength},
978 {"maxIsolation", rim.maxIsolation},
979 {"maxIsolationFace", rim.maxIsolationFace},
980 {"maxIsolationPoint", rim.maxIsolationPoint}});
981 }
982 if (!navigable) {
983 unreliableParts.push_back(part.id + " (" + reliabilityName + ")");
984 }
985
986 SampleConfig cfg;
987 cfg.nBulk = opt.points;
988 cfg.nBoundary = opt.points;
989 cfg.nInside = std::max(1, opt.points / 2);
990 cfg.nOutsideRays = opt.rays;
991 cfg.nInsideRays = std::max(1, opt.rays / 2);
992 cfg.seed = opt.seed;
993 const SampleSet samples = opt.loadSamples.empty() ? generateSamples(reference, bboxMin, bboxMax, cfg)
994 : readSamples(opt.loadSamples, part.id);
995
996 long long candidatesSampled = 0;
997 const size_t nProbe = std::min<size_t>(200, samples.outsideRays.size());
998 for (size_t i = 0; i < nProbe; ++i) {
999 const auto& r = samples.outsideRays[i];
1000 const int n = surf.CountBVHRayCandidates(r.origin, r.dir);
1001 if (n > 0) {
1002 candidatesSampled += n;
1003 }
1004 }
1005 std::printf(" BVH ray candidates: sum=%lld over %zu probe rays\n", candidatesSampled, nProbe);
1006
1007 json partJson;
1008 partJson["id"] = part.id;
1009 partJson["model"] = part.model;
1010 partJson["nSurfaces"] = surf.GetNsurfaces();
1011 partJson["nTriangles"] = mesh.GetNfacets();
1012 partJson["closeShapeSecondsSurface"] = surfCloseSeconds;
1013 partJson["closeShapeSecondsMesh"] = meshCloseSeconds;
1014 partJson["bvhRayCandidatesSampled"] = candidatesSampled;
1015 partJson["bvhRayCandidatesProbeRays"] = nProbe;
1016 partJson["navigation"] = {{"reliability", reliabilityName},
1017 {"navigable", navigable},
1018 {"boundaryEdges", surf.GetBoundaryEdgeCount()},
1019 {"nonManifoldEdges", surf.GetNonManifoldEdgeCount()},
1020 {"reversedEdges", surf.GetReversedEdgeCount()},
1021 {"maxRimIsolation", surf.GetMaxRimIsolation()},
1022 {"rimChordResolution", surf.GetRimChordResolution()},
1023 {"rimMatchTolerance", surf.GetRimMatchTolerance()},
1024 {"totalRimLength", surf.GetTotalRimLength()},
1025 {"unmatchedRimLength", surf.GetUnmatchedRimLength()},
1026 {"rims", surf.GetRimCount()},
1027 {"matchedRims", surf.GetMatchedRimCount()},
1028 {"boundaryRims", surf.GetBoundaryRimCount()},
1029 {"nonManifoldRims", surf.GetNonManifoldRimCount()},
1030 {"reversedRims", surf.GetReversedRimCount()},
1031 {"hasEdgeIdentity", surf.HasEdgeIdentity()},
1032 {"sourceEdges", surf.GetSourceEdgeCount()},
1033 {"sharedSourceEdges", surf.GetSharedSourceEdgeCount()},
1034 {"boundarySourceEdges", surf.GetBoundarySourceEdgeCount()},
1035 {"nonManifoldSourceEdges", surf.GetNonManifoldSourceEdgeCount()},
1036 {"reversedSourceEdges", surf.GetReversedSourceEdgeCount()},
1037 {"degenerateSourceEdges", surf.GetDegenerateSourceEdgeCount()},
1038 {"maxSharedEdgeDeviation", surf.GetMaxSharedEdgeDeviation()},
1039 {"rimDetail", rimsJson}};
1040
1041 std::vector<Point3D> allPoints = samples.bulkPoints;
1042 allPoints.insert(allPoints.end(), samples.boundaryPoints.begin(), samples.boundaryPoints.end());
1043 allPoints.insert(allPoints.end(), samples.insidePoints.begin(), samples.insidePoints.end());
1044 const std::array<size_t, 3> categorySizes{samples.bulkPoints.size(), samples.boundaryPoints.size(),
1045 samples.insidePoints.size()};
1046
1047 if (!opt.dumpSamples.empty()) {
1048 writeSamples(opt.dumpSamples, part.id, samples);
1049 }
1050
1051 // Ground-truth validation, when the oracle has answered this part. Kept separate from the
1052 // mesh comparison below rather than replacing it: the mesh columns stay comparable with every
1053 // measurement recorded so far, while these columns are the ones a gate can be written against.
1054 if (!opt.refAnswers.empty()) {
1055 const OracleAnswers oracle = loadOracleAnswers(opt.refAnswers, part.id);
1056 if (oracle.has) {
1057 ValidationOptions oracleOpt;
1058 // The band is now the model's own declared tolerance instead of a guessed mesh sagitta.
1059 // A floor keeps a perfectly-toleranced synthetic fixture from demanding bit equality.
1060 oracleOpt.meshBand = std::max(oracle.tolerance, oracleOpt.distanceTolerance);
1061 const auto boundaryDistance =
1062 mergeCategories<double>(oracle.boundaryDistance, categorySizes, -1.);
1063 const auto containsState = mergeCategories<int>(oracle.containsState, categorySizes, -1);
1064
1065 std::printf(" oracle: %s tolerance=%.3g capacity=%.6g cm^3 (band=%.3g)\n",
1066 oracle.valid ? "valid" : "*** NOT BRepCheck-VALID ***", oracle.tolerance,
1067 oracle.capacity, oracleOpt.meshBand);
1068
1069 // The historical block, unchanged in content: the exact-surface representation's columns
1070 // under `oracle`, printed with the same "O:" labels. Everything written here before this
1071 // refactor is still written here, by the same code, so the existing path is inert.
1072 json oracleJson = scoreAgainstOracle(candidate, oracle, oracleOpt, allPoints, containsState,
1073 boundaryDistance, samples, opt.only, "O", "oracle");
1074 partJson["oracle"] = oracleJson;
1075
1076 // New: the same four columns for every other representation the part has, against the
1077 // same answers. This is what makes a CSG-emitted or tessellated part scoreable at all,
1078 // and it is the shape the tiered coverage scorecard needs -- parallel columns, not
1079 // alternatives behind a flag.
1080 json representationsJson = json::array();
1081 for (const auto& rep : representations) {
1082 const bool isSurface = rep.surfaceSolid != nullptr;
1083 json repJson;
1084 repJson["name"] = rep.name;
1085 repJson["source"] = rep.source;
1086 repJson["shapeClass"] = rep.shape->ClassName();
1087 if (rep.primitives >= 0) {
1088 repJson["primitives"] = rep.primitives;
1089 repJson["primitiveKind"] = rep.primitiveKind;
1090 }
1091 const auto capacityKind = capacityKindOf(rep);
1092 repJson["capacityMethod"] = capacityKind.method;
1093 repJson["capacityComparable"] = capacityKind.comparable;
1094 // The frame check, per representation: a candidate whose box does not sit where the
1095 // oracle's box sits is not being asked the same questions the oracle answered.
1096 repJson["bboxDeviationFromOracle"] =
1097 bboxDeviationFromOracle(rep.shape, oracle, rep.placement);
1098 // The placement, mirrored into the scorecard as a 3x4 row-major [R | t] so that a
1099 // Python consumer never has to open the .root file, and null when there is none.
1100 if (rep.placement != nullptr) {
1101 const double* rot = rep.placement->GetRotationMatrix();
1102 const double* tr = rep.placement->GetTranslation();
1103 repJson["placement"] = {{rot[0], rot[1], rot[2], tr[0]},
1104 {rot[3], rot[4], rot[5], tr[1]},
1105 {rot[6], rot[7], rot[8], tr[2]}};
1106 } else {
1107 repJson["placement"] = nullptr;
1108 }
1109
1110 // Closure / rims / NavigationReliability are O2BVHSurfaceSolid concepts. A
1111 // TGeoCompositeShape has neither, and a triangle mesh has a different notion entirely,
1112 // so those keys exist only where the question has an answer. `closureApplicable`
1113 // records the decision explicitly instead of leaving a reader to infer it from an
1114 // absent field.
1115 repJson["closureApplicable"] = isSurface;
1116 if (isSurface) {
1117 repJson["reliability"] = reliabilityName;
1118 repJson["navigable"] = navigable;
1119 } else if (rep.name == "mesh") {
1120 // O2Tessellated's own, differently-named watertightness statement. Deliberately not
1121 // called `navigable`: it is a property of the triangle soup, decided by half-edge
1122 // counting over chords, and it is not the same claim.
1123 repJson["meshClosedBody"] = mesh.IsClosedBody();
1124 }
1125
1126 if (isSurface) {
1127 // Already computed above; scoring the same shape twice would only cost time and
1128 // invite the two copies to drift.
1129 repJson["oracle"] = oracleJson;
1130 } else {
1131 std::printf(" --- representation '%s' (%s) against the same oracle answers ---\n",
1132 rep.name.c_str(), rep.shape->ClassName());
1133 repJson["oracle"] = scoreAgainstOracle(rep.shape, oracle, oracleOpt, allPoints,
1134 containsState, boundaryDistance, samples,
1135 opt.only, "R:" + rep.name,
1136 "oracle[" + rep.name + "]", rep.placement);
1137 }
1138 repJson["disagreements"] = countDisagreements(repJson["oracle"]);
1139 representationsJson.push_back(std::move(repJson));
1140 }
1141 partJson["representations"] = std::move(representationsJson);
1142 }
1143 }
1144
1145 if (opt.only.count("contains")) {
1146 auto v = validateContains(candidate, reference, allPoints);
1147 printValidation("contains", v);
1148 auto tc = timeContains(candidate, allPoints, opt.warmup, opt.repeat);
1149 auto tr = timeContains(reference, allPoints, opt.warmup, opt.repeat);
1150 printTiming("contains", tc, tr);
1151 partJson["contains"] = {{"validation", validationToJson(v)},
1152 {"timingCandidate", timingToJson(tc)},
1153 {"timingReference", timingToJson(tr)}};
1154 }
1155 if (opt.only.count("distout")) {
1156 auto v = validateDistFromOutside(candidate, reference, samples.outsideRays);
1157 printValidation("distout", v);
1158 auto tc = timeDistFromOutside(candidate, samples.outsideRays, opt.warmup, opt.repeat);
1159 auto tr = timeDistFromOutside(reference, samples.outsideRays, opt.warmup, opt.repeat);
1160 printTiming("distout", tc, tr);
1161 // the all-surfaces baseline: what the BVH traversal buys over visiting every patch
1162 auto tl = timeRayKernel(samples.outsideRays, opt.warmup, opt.repeat,
1163 [&surf](const Point3D& o, const Point3D& d) {
1164 return surf.DistFromOutside_Loop(o.data(), d.data());
1165 });
1166 printLoopSpeedup("distout", tc, tl);
1167 partJson["distout"] = {{"validation", validationToJson(v)},
1168 {"timingCandidate", timingToJson(tc)},
1169 {"timingReference", timingToJson(tr)},
1170 {"timingCandidateLoop", timingToJson(tl)}};
1171 }
1172 if (opt.only.count("distin")) {
1173 auto v = validateDistFromInside(candidate, reference, samples.insideRays);
1174 printValidation("distin", v);
1175 auto tc = timeDistFromInside(candidate, samples.insideRays, opt.warmup, opt.repeat);
1176 auto tr = timeDistFromInside(reference, samples.insideRays, opt.warmup, opt.repeat);
1177 printTiming("distin", tc, tr);
1178 auto tl = timeRayKernel(samples.insideRays, opt.warmup, opt.repeat,
1179 [&surf](const Point3D& o, const Point3D& d) {
1180 return surf.DistFromInside_Loop(o.data(), d.data());
1181 });
1182 printLoopSpeedup("distin", tc, tl);
1183 partJson["distin"] = {{"validation", validationToJson(v)},
1184 {"timingCandidate", timingToJson(tc)},
1185 {"timingReference", timingToJson(tr)},
1186 {"timingCandidateLoop", timingToJson(tl)}};
1187 }
1188 if (opt.only.count("safety")) {
1189 // Never compared against each other (see ground rules): each shape's Safety() is checked
1190 // against its own DistFrom{Inside,Outside} contract independently.
1191 auto vc = validateSafety(candidate, allPoints);
1192 auto vr = validateSafety(reference, allPoints);
1193 printValidation("safety(cand)", vc);
1194 printValidation("safety(ref)", vr);
1195 auto tc = timeSafety(candidate, allPoints, opt.warmup, opt.repeat);
1196 auto tr = timeSafety(reference, allPoints, opt.warmup, opt.repeat);
1197 printTiming("safety", tc, tr);
1198 partJson["safety"] = {{"validationCandidate", validationToJson(vc)},
1199 {"validationReference", validationToJson(vr)},
1200 {"timingCandidate", timingToJson(tc)},
1201 {"timingReference", timingToJson(tr)}};
1202 }
1203
1204 if (opt.loopCrosscheck) {
1205 // Independent of the tessellated reference entirely: separates
1206 // BVH/traversal bugs from surface-kernel bugs.
1207 // The distance twins must agree *exactly*, not within a tolerance: both take a
1208 // minimum over the same hits from the same kernels and differ only in which surfaces the
1209 // BVH lets them skip, so any difference at all is a traversal or pruning bug.
1210 size_t containsAgree = 0;
1211 size_t crossingDumps = 0;
1212 constexpr size_t kMaxCrossingDumps = 3;
1213 std::vector<O2BVHSurfaceSolid::ContainsCrossing> bvhCrossings;
1214 std::vector<O2BVHSurfaceSolid::ContainsCrossing> loopCrossings;
1215 for (const auto& p : allPoints) {
1216 if (surf.Contains(p.data()) == surf.Contains_Loop(p.data())) {
1217 ++containsAgree;
1218 continue;
1219 }
1220 // A parity disagreement between two paths over the same kernels means the two hit lists
1221 // differ. Print them: the difference is the diagnosis, and guessing at it has already
1222 // cost this project one three-item plan built on a wrong premise.
1223 if (crossingDumps++ >= kMaxCrossingDumps) {
1224 continue;
1225 }
1226 surf.DescribeContainsCrossings(p, bvhCrossings, loopCrossings);
1227 std::printf(" BVH!=Loop at (%.9g,%.9g,%.9g): BVH=%d (%zu crossings) Loop=%d (%zu crossings)\n",
1228 p[0], p[1], p[2], static_cast<int>(surf.Contains(p.data())), bvhCrossings.size(),
1229 static_cast<int>(surf.Contains_Loop(p.data())), loopCrossings.size());
1230 const size_t nShow = std::max(bvhCrossings.size(), loopCrossings.size());
1231 for (size_t i = 0; i < nShow; ++i) {
1232 const char* bvhKind = i < bvhCrossings.size()
1233 ? (bvhCrossings[i].normalAlignment < 0. ? "ENTER" : "EXIT ")
1234 : "-----";
1235 const char* loopKind = i < loopCrossings.size()
1236 ? (loopCrossings[i].normalAlignment < 0. ? "ENTER" : "EXIT ")
1237 : "-----";
1238 const double bvhT = i < bvhCrossings.size() ? bvhCrossings[i].distance : -1.;
1239 const double loopT = i < loopCrossings.size() ? loopCrossings[i].distance : -1.;
1240 std::printf(" [%2zu] BVH %s t=%-18.12g Loop %s t=%-18.12g%s\n", i, bvhKind, bvhT,
1241 loopKind, loopT,
1242 (i < bvhCrossings.size() && i < loopCrossings.size() &&
1243 std::fabs(bvhT - loopT) > 1.e-12)
1244 ? " <-- differs"
1245 : "");
1246 }
1247 }
1248 std::printf(" loop-crosscheck contains: BVH==Loop for %zu/%zu points\n", containsAgree, allPoints.size());
1249 partJson["loopCrosscheckContains"] = {{"agree", containsAgree}, {"total", allPoints.size()}};
1250
1251 size_t outAgree = 0;
1252 double worstOutDeviation = 0.;
1253 for (const auto& r : samples.outsideRays) {
1254 const double bvh = surf.DistFromOutside(r.origin.data(), r.dir.data(), 3);
1255 const double loop = surf.DistFromOutside_Loop(r.origin.data(), r.dir.data());
1256 if (bvh == loop) {
1257 ++outAgree;
1258 } else {
1259 worstOutDeviation = std::max(worstOutDeviation, std::fabs(bvh - loop));
1260 }
1261 }
1262 std::printf(" loop-crosscheck distout : BVH==Loop for %zu/%zu rays (worstDev=%.6g)\n", outAgree,
1263 samples.outsideRays.size(), worstOutDeviation);
1264 partJson["loopCrosscheckDistOutside"] = {
1265 {"agree", outAgree}, {"total", samples.outsideRays.size()}, {"worstDeviation", worstOutDeviation}};
1266
1267 size_t inAgree = 0;
1268 double worstInDeviation = 0.;
1269 for (const auto& r : samples.insideRays) {
1270 const double bvh = surf.DistFromInside(r.origin.data(), r.dir.data(), 3);
1271 const double loop = surf.DistFromInside_Loop(r.origin.data(), r.dir.data());
1272 if (bvh == loop) {
1273 ++inAgree;
1274 } else {
1275 worstInDeviation = std::max(worstInDeviation, std::fabs(bvh - loop));
1276 }
1277 }
1278 std::printf(" loop-crosscheck distin : BVH==Loop for %zu/%zu rays (worstDev=%.6g)\n", inAgree,
1279 samples.insideRays.size(), worstInDeviation);
1280 partJson["loopCrosscheckDistInside"] = {
1281 {"agree", inAgree}, {"total", samples.insideRays.size()}, {"worstDeviation", worstInDeviation}};
1282 }
1283
1284 if (opt.pruningAb) {
1285 // Prices the ray tmax tightening: the same rays run with it on and off, reporting both the
1286 // surface patches the traversal actually handed to the leaf callback and the wall time. The
1287 // answers must be bit-identical -- the switch is a cost knob, never a semantic one, and a
1288 // mismatch here is a bug in the tightening rather than a measurement.
1289 json pruningJson;
1290 size_t identical = 0;
1291 std::vector<double> prunedValues;
1292 prunedValues.reserve(samples.outsideRays.size());
1293
1296 for (const auto& r : samples.outsideRays) {
1297 prunedValues.push_back(surf.DistFromOutside(r.origin.data(), r.dir.data(), 3));
1298 }
1299 const long long prunedCandidates = O2BVHSurfaceSolid::GetRayCandidateCount();
1300 auto tPruned = timeDistFromOutside(candidate, samples.outsideRays, opt.warmup, opt.repeat);
1301
1304 for (size_t i = 0; i < samples.outsideRays.size(); ++i) {
1305 const auto& r = samples.outsideRays[i];
1306 if (surf.DistFromOutside(r.origin.data(), r.dir.data(), 3) == prunedValues[i]) {
1307 ++identical;
1308 }
1309 }
1310 const long long unprunedCandidates = O2BVHSurfaceSolid::GetRayCandidateCount();
1311 auto tUnpruned = timeDistFromOutside(candidate, samples.outsideRays, opt.warmup, opt.repeat);
1313
1314 const double candidateRatio =
1315 unprunedCandidates > 0 ? static_cast<double>(prunedCandidates) / static_cast<double>(unprunedCandidates) : 0.;
1316 const double speedup = tPruned.nsPerCall > 0. ? tUnpruned.nsPerCall / tPruned.nsPerCall : 0.;
1317 std::printf(" tmax-pruning A/B (distout, %zu rays): identical=%zu/%zu\n", samples.outsideRays.size(), identical,
1318 samples.outsideRays.size());
1319 std::printf(" candidates: pruned=%lld unpruned=%lld (%.1f%% of the work)\n", prunedCandidates,
1320 unprunedCandidates, 100. * candidateRatio);
1321 std::printf(" time : pruned=%9.1f ns/call unpruned=%9.1f ns/call speedup=%.2fx\n",
1322 tPruned.nsPerCall, tUnpruned.nsPerCall, speedup);
1323
1324 pruningJson["identical"] = identical;
1325 pruningJson["total"] = samples.outsideRays.size();
1326 pruningJson["candidatesPruned"] = prunedCandidates;
1327 pruningJson["candidatesUnpruned"] = unprunedCandidates;
1328 pruningJson["timingPruned"] = timingToJson(tPruned);
1329 pruningJson["timingUnpruned"] = timingToJson(tUnpruned);
1330 partJson["tmaxPruningAB"] = std::move(pruningJson);
1331 }
1332
1333 jsonReport.push_back(std::move(partJson));
1334 }
1335
1336 // Repeated at the end because per-part lines scroll away in a 19-part run, and because the whole
1337 // point of item 4 is that no future reader can attribute an "unexplained" column to mesh
1338 // chording without first seeing whether the subject was a closed manifold at all.
1339 if (!unreliableParts.empty()) {
1340 std::printf(
1341 "\n*** %zu of %zu part(s) are NOT navigable; their accuracy columns above measure an\n"
1342 "*** undefined answer, not the exact solid's error.\n",
1343 unreliableParts.size(), parts.size());
1344 for (const auto& id : unreliableParts) {
1345 std::printf("*** %s\n", id.c_str());
1346 }
1347 } else {
1348 std::printf("\nAll %zu part(s) closed consistently oriented manifolds: navigation results are meaningful.\n",
1349 parts.size());
1350 }
1351
1352 // The tiered scorecard, in its most compact form: how many disagreements outside tolerance each
1353 // representation of each part has. Printed here because the per-part blocks scroll away, and
1354 // because "which representation would have accepted this part" is the question the converter's
1355 // dispatch policy will be written against.
1356 bool anyRepresentations = false;
1357 for (const auto& partJson : jsonReport) {
1358 anyRepresentations = anyRepresentations || partJson.contains("representations");
1359 }
1360 if (anyRepresentations) {
1361 std::printf("\n=== REPRESENTATION SCORECARD (disagreements outside tolerance, all four columns) ===\n");
1362 for (const auto& partJson : jsonReport) {
1363 if (!partJson.contains("representations")) {
1364 continue;
1365 }
1366 std::printf(" %-46s", partJson.at("id").get<std::string>().c_str());
1367 for (const auto& rep : partJson.at("representations")) {
1368 const double capacityDeviation =
1369 rep.at("oracle").value("capacityRelativeDeviation", 0.);
1370 const bool capacityComparable = rep.value("capacityComparable", false);
1371 char capacityText[32];
1372 if (capacityComparable) {
1373 std::snprintf(capacityText, sizeof(capacityText), "%.2g", std::fabs(capacityDeviation));
1374 } else {
1375 std::snprintf(capacityText, sizeof(capacityText), "n/a");
1376 }
1377 std::printf(" %s=%zu (cap %s)", rep.at("name").get<std::string>().c_str(),
1378 rep.value("disagreements", size_t{0}), capacityText);
1379 }
1380 std::printf("\n");
1381 }
1382 }
1383
1384 if (!opt.jsonOut.empty()) {
1385 std::ofstream out(opt.jsonOut);
1386 out << jsonReport.dump(1);
1387 std::printf("\nWrote %s\n", opt.jsonOut.c_str());
1388 }
1389
1390 return 0;
1391}
int32_t i
bool valid
Validation and timing harness for TGeoShape navigation, typed on plain TGeoShape*.
bool fileExists(const char *filename)
uint32_t j
Definition RawData.h:0
uint32_t c
Definition RawData.h:2
uint32_t version
Definition RawData.h:8
StringRef key
void CloseShape(bool check=true, bool fixFlipped=true, bool verbose=true)
Close the shape: calculate bounding box and compact vertices.
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 const char * GetNavigationReliabilityName(NavigationReliability reliability)
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
const GLdouble * v
Definition glcorearb.h:832
GLenum array
Definition glcorearb.h:4274
GLuint const GLchar * name
Definition glcorearb.h:781
GLsizei samples
Definition glcorearb.h:1309
GLsizei GLsizei GLchar * source
Definition glcorearb.h:798
GLenum GLsizei GLsizei GLint * values
Definition glcorearb.h:1576
GLuint GLsizei const GLchar * label
Definition glcorearb.h:2519
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
ValidationResult validateContainsAgainstOracle(const TGeoShape *candidate, const std::vector< Point3D > &points, const std::vector< int > &oracleState, const std::vector< double > &oracleBoundaryDistance, const ValidationOptions &opt={})
oracleState: 1 inside, 0 outside, -1 declined; oracleBoundaryDistance may cover only a prefix of poin...
SampleSet generateSamples(const TGeoShape *reference, const Point3D &bboxMin, const Point3D &bboxMax, const SampleConfig &cfg={})
A deterministic sample set from cfg.seed and the bbox; reference, the trusted mesh,...
ValidationResult validateSafetyAgainstOracle(const TGeoShape *candidate, const std::vector< Point3D > &points, const std::vector< double > &oracleBoundaryDistance, const ValidationOptions &opt={})
Safety's contract against the oracle's exact distance: 0 <= safety <= trueDistance.
TimingResult timeDistFromInside(const TGeoShape *shape, const std::vector< Ray > &rays, int warmupRepeats, int timedRepeats)
TimingResult timeDistFromOutside(const TGeoShape *shape, const std::vector< Ray > &rays, int warmupRepeats, int timedRepeats, double stepmax=TGeoShape::Big())
ValidationResult validateSafety(const TGeoShape *shape, const std::vector< Point3D > &points, const ValidationOptions &opt={})
Check one shape's Safety() lower-bound contract against its own DistFrom* along six probe directions;...
ValidationResult validateDistFromInside(const TGeoShape *candidate, const TGeoShape *reference, const std::vector< Ray > &rays, const ValidationOptions &opt={})
ValidationResult validateDistanceAgainstOracle(const TGeoShape *candidate, const std::vector< Ray > &rays, const std::vector< double > &oracleDistance, bool wantInside, const ValidationOptions &opt={}, const std::vector< int > &oracleOriginState={})
TimingResult timeContains(const TGeoShape *shape, const std::vector< Point3D > &points, int warmupRepeats, int timedRepeats)
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...
ValidationResult validateContains(const TGeoShape *candidate, const TGeoShape *reference, const std::vector< Point3D > &points, const ValidationOptions &opt={})
TimingResult timeRayKernel(const std::vector< Ray > &rays, int warmupRepeats, int timedRepeats, RayKernel &&kernel)
Time a per-ray kernel kernel(origin, dir) exactly like the timeDistFrom* functions,...
ValidationResult validateDistFromOutside(const TGeoShape *candidate, const TGeoShape *reference, const std::vector< Ray > &rays, const ValidationOptions &opt={})
TimingResult timeSafety(const TGeoShape *shape, const std::vector< Point3D > &points, int warmupRepeats, int timedRepeats)
std::array< double, 3 > Point3D
bool LoadFacetSolid(const std::string &file, o2::base::O2Tessellated &solid)
bool LoadSurfaceSolid(const std::string &file, O2BVHSurfaceSolid &solid)
std::string to_string(gsl::span< T, Size > span)
Definition common.h:52
nlohmann::json json
Parameters of generateSamples; the counts are targets, and a category may come back short.
int nInsideRays
rays from inside origins, for DistFromInside
int nOutsideRays
rays from outside origins, for DistFromOutside
int nBulk
uniform points over the inflated bbox
int nBoundary
points within boundaryBand of the reference surface
uint64_t seed
every SampleSet is fully determined by this and the bbox
int nInside
points accepted by the reference Contains()
std::vector< Ray > outsideRays
std::vector< Ray > insideRays
uint64_t checksum
accumulated from the results so the optimizer cannot elide the calls
double distanceTolerance
absolute agreement tolerance for distances (cm)
std::map< std::string, ID > expected
#define main