Project
Loading...
Searching...
No Matches
testFlatCSG.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
14#define BOOST_TEST_MODULE Test O2FlatCSG class
15#define BOOST_TEST_MAIN
16#define BOOST_TEST_DYN_LINK
17#include <boost/test/unit_test.hpp>
18
21
22#include "TFile.h"
23#include "TGeoBBox.h"
24#include "TGeoShape.h"
25#include "TGeoTorus.h"
26#include "TGeoTube.h"
27#include "TMath.h"
28
29#include <algorithm>
30#include <cmath>
31#include <filesystem>
32#include <limits>
33#include <vector>
34
35namespace
36{
38
40class Rng
41{
42 public:
43 explicit Rng(unsigned long long seed) : mState(seed) {}
44 double uniform(double low, double high)
45 {
46 mState = mState * 6364136223846793005ULL + 1442695040888963407ULL;
47 const double unit = static_cast<double>((mState >> 11) & ((1ULL << 53) - 1)) / static_cast<double>(1ULL << 53);
48 return low + unit * (high - low);
49 }
50
51 private:
52 unsigned long long mState;
53};
54
56void planeQuadric(const double n[3], const double p[3], double coeff[10])
57{
58 for (int index = 0; index < 6; ++index) {
59 coeff[index] = 0.;
60 }
61 coeff[6] = 0.5 * n[0];
62 coeff[7] = 0.5 * n[1];
63 coeff[8] = 0.5 * n[2];
64 coeff[9] = -(n[0] * p[0] + n[1] * p[1] + n[2] * p[2]);
65}
66
68void zCylinderQuadric(double r, double coeff[10])
69{
70 const double values[10] = {1., 0., 0., 1., 0., 0., 0., 0., 0., -r * r};
71 for (int index = 0; index < 10; ++index) {
72 coeff[index] = values[index];
73 }
74}
75
81void tiltedCylinderQuadric(double r, double coeff[10])
82{
83 const double s = 1. / std::sqrt(3.);
84 const double d[3] = {s, s, s};
85 double a[3][3];
86 for (int row = 0; row < 3; ++row) {
87 for (int column = 0; column < 3; ++column) {
88 a[row][column] = (row == column ? 1. : 0.) - d[row] * d[column];
89 }
90 }
91 coeff[0] = a[0][0];
92 coeff[1] = a[0][1];
93 coeff[2] = a[0][2];
94 coeff[3] = a[1][1];
95 coeff[4] = a[1][2];
96 coeff[5] = a[2][2];
97 coeff[6] = 0.;
98 coeff[7] = 0.;
99 coeff[8] = 0.;
100 coeff[9] = -r * r;
101}
102
104void addBoxCell(O2FlatCSG& solid, double dx, double dy, double dz)
105{
106 const double half[3] = {dx, dy, dz};
107 const int first = solid.GetNhalfspaces();
108 for (int axis = 0; axis < 3; ++axis) {
109 for (int sense = -1; sense <= 1; sense += 2) {
110 double normal[3] = {0., 0., 0.};
111 double through[3] = {0., 0., 0.};
112 normal[axis] = static_cast<double>(sense);
113 through[axis] = sense * half[axis];
114 double coeff[10];
115 planeQuadric(normal, through, coeff);
116 solid.AddQuadric(1., coeff);
117 }
118 }
119 solid.AddCell(first, 6, 8. * dx * dy * dz);
120}
121} // namespace
122
123BOOST_AUTO_TEST_CASE(box_from_six_planes_contains_like_TGeoBBox)
124{
125 O2FlatCSG solid("box");
126 addBoxCell(solid, 3., 4., 5.);
127 BOOST_CHECK_EQUAL(solid.GetNcells(), 1);
128 BOOST_CHECK_EQUAL(solid.GetNhalfspaces(), 6);
129
130 TGeoBBox reference(3., 4., 5.);
131 Rng rng(20260824ULL);
132 int scored = 0;
133 for (int trial = 0; trial < 20000; ++trial) {
134 const double point[3] = {rng.uniform(-6., 6.), rng.uniform(-7., 7.), rng.uniform(-8., 8.)};
135 // skip the boundary shell, where the two shapes are allowed to disagree by tolerance
136 if (std::abs(std::abs(point[0]) - 3.) < 1.e-9 || std::abs(std::abs(point[1]) - 4.) < 1.e-9 ||
137 std::abs(std::abs(point[2]) - 5.) < 1.e-9) {
138 continue;
139 }
140 BOOST_REQUIRE_EQUAL(solid.Contains_Loop(point), reference.Contains(point));
141 BOOST_REQUIRE_EQUAL(solid.Contains(point), solid.Contains_Loop(point));
142 ++scored;
143 }
144 BOOST_CHECK_GT(scored, 19000);
145}
146
147BOOST_AUTO_TEST_CASE(tube_from_two_cylinders_and_two_planes_contains_like_TGeoTube)
148{
149 // rmin = 2, rmax = 5, dz = 7: the inner cylinder is a COMPLEMENTED halfspace, which is what
150 // makes this cell non-convex and is the case the whole class exists for.
151 O2FlatCSG solid("tube");
152 double coeff[10];
153 zCylinderQuadric(5., coeff);
154 solid.AddQuadric(1., coeff);
155 zCylinderQuadric(2., coeff);
156 solid.AddQuadric(-1., coeff);
157 const double up[3] = {0., 0., 1.};
158 const double down[3] = {0., 0., -1.};
159 const double top[3] = {0., 0., 7.};
160 const double bottom[3] = {0., 0., -7.};
161 planeQuadric(up, top, coeff);
162 solid.AddQuadric(1., coeff);
163 planeQuadric(down, bottom, coeff);
164 solid.AddQuadric(1., coeff);
165 solid.AddCell(0, 4, TMath::Pi() * (25. - 4.) * 14.);
166
167 TGeoTube reference(2., 5., 7.);
168 Rng rng(777ULL);
169 for (int trial = 0; trial < 20000; ++trial) {
170 const double point[3] = {rng.uniform(-6., 6.), rng.uniform(-6., 6.), rng.uniform(-8., 8.)};
171 const double radius = std::hypot(point[0], point[1]);
172 if (std::abs(radius - 2.) < 1.e-9 || std::abs(radius - 5.) < 1.e-9 ||
173 std::abs(std::abs(point[2]) - 7.) < 1.e-9) {
174 continue;
175 }
176 BOOST_TEST_CONTEXT("point = (" << point[0] << ", " << point[1] << ", " << point[2] << ")")
177 {
178 BOOST_REQUIRE_EQUAL(solid.Contains_Loop(point), reference.Contains(point));
179 }
180 }
181}
182
183BOOST_AUTO_TEST_CASE(two_disjoint_cells_are_a_union)
184{
185 O2FlatCSG solid("two_boxes");
186 addBoxCell(solid, 1., 1., 1.);
187 // a second box, centred at x = +10, as six planes of its own
188 const int first = solid.GetNhalfspaces();
189 const double centre = 10.;
190 for (int axis = 0; axis < 3; ++axis) {
191 for (int sense = -1; sense <= 1; sense += 2) {
192 double normal[3] = {0., 0., 0.};
193 double through[3] = {centre, 0., 0.};
194 normal[axis] = static_cast<double>(sense);
195 through[axis] += (axis == 0 ? sense * 1. : 0.);
196 if (axis != 0) {
197 through[axis] = sense * 1.;
198 }
199 double coeff[10];
200 planeQuadric(normal, through, coeff);
201 solid.AddQuadric(1., coeff);
202 }
203 }
204 solid.AddCell(first, 6, 8.);
205
206 const double inFirst[3] = {0., 0., 0.};
207 const double inSecond[3] = {10., 0., 0.};
208 const double between[3] = {5., 0., 0.};
209 BOOST_CHECK(solid.Contains_Loop(inFirst));
210 BOOST_CHECK(solid.Contains_Loop(inSecond));
211 BOOST_CHECK(!solid.Contains_Loop(between));
212}
213
214BOOST_AUTO_TEST_CASE(box_distances_match_TGeoBBox)
215{
216 O2FlatCSG solid("box_dist");
217 addBoxCell(solid, 3., 4., 5.);
218 TGeoBBox reference(3., 4., 5.);
219
220 Rng rng(4242ULL);
221 for (int trial = 0; trial < 20000; ++trial) {
222 double point[3] = {rng.uniform(-12., 12.), rng.uniform(-12., 12.), rng.uniform(-12., 12.)};
223 double dir[3];
224 double norm = 0.;
225 do {
226 for (int index = 0; index < 3; ++index) {
227 dir[index] = rng.uniform(-1., 1.);
228 }
229 norm = std::sqrt(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]);
230 } while (norm < 1.e-3);
231 for (int index = 0; index < 3; ++index) {
232 dir[index] /= norm;
233 }
234 const bool inside = reference.Contains(point);
235 if (inside != static_cast<bool>(solid.Contains_Loop(point))) {
236 continue; // a boundary point; classification is tested separately
237 }
238 const double mine = inside ? solid.DistFromInside_Loop(point, dir, TGeoShape::Big())
239 : solid.DistFromOutside_Loop(point, dir, TGeoShape::Big());
240 const double theirs = inside ? reference.DistFromInside(point, dir, 3, TGeoShape::Big(), nullptr)
241 : reference.DistFromOutside(point, dir, 3, TGeoShape::Big(), nullptr);
242 if (theirs >= TGeoShape::Big()) {
243 BOOST_REQUIRE_GE(mine, TGeoShape::Big());
244 } else {
245 BOOST_REQUIRE_SMALL(mine - theirs, 1.e-9);
246 }
247 }
248}
249
250BOOST_AUTO_TEST_CASE(tube_distances_match_TGeoTube_through_the_bore)
251{
252 // the complemented inner cylinder makes the occupancy along a ray TWO intervals for a ray that
253 // crosses the bore, which is the case a convexity assumption would get wrong
254 O2FlatCSG solid("tube_dist");
255 double coeff[10];
256 zCylinderQuadric(5., coeff);
257 solid.AddQuadric(1., coeff);
258 zCylinderQuadric(2., coeff);
259 solid.AddQuadric(-1., coeff);
260 const double up[3] = {0., 0., 1.};
261 const double down[3] = {0., 0., -1.};
262 const double top[3] = {0., 0., 7.};
263 const double bottom[3] = {0., 0., -7.};
264 planeQuadric(up, top, coeff);
265 solid.AddQuadric(1., coeff);
266 planeQuadric(down, bottom, coeff);
267 solid.AddQuadric(1., coeff);
268 solid.AddCell(0, 4, 0.);
269
270 TGeoTube reference(2., 5., 7.);
271 // a ray straight along +x at z = 0 enters the wall at x = -5, leaves it at x = -2, re-enters at
272 // x = +2 and leaves at x = +5
273 const double origin[3] = {-9., 0., 0.};
274 const double dir[3] = {1., 0., 0.};
275 BOOST_CHECK_SMALL(solid.DistFromOutside_Loop(origin, dir, TGeoShape::Big()) - 4., 1.e-12);
276
277 const double inWall[3] = {-4., 0., 0.};
278 BOOST_CHECK_SMALL(solid.DistFromInside_Loop(inWall, dir, TGeoShape::Big()) - 2., 1.e-12);
279
280 const double inBore[3] = {0., 0., 0.};
281 BOOST_CHECK(!solid.Contains_Loop(inBore));
282 BOOST_CHECK_SMALL(solid.DistFromOutside_Loop(inBore, dir, TGeoShape::Big()) - 2., 1.e-12);
283
284 Rng rng(99ULL);
285 for (int trial = 0; trial < 20000; ++trial) {
286 double point[3] = {rng.uniform(-9., 9.), rng.uniform(-9., 9.), rng.uniform(-10., 10.)};
287 double direction[3];
288 double norm = 0.;
289 do {
290 for (int index = 0; index < 3; ++index) {
291 direction[index] = rng.uniform(-1., 1.);
292 }
293 norm = std::sqrt(direction[0] * direction[0] + direction[1] * direction[1] + direction[2] * direction[2]);
294 } while (norm < 1.e-3);
295 for (int index = 0; index < 3; ++index) {
296 direction[index] /= norm;
297 }
298 const bool inside = reference.Contains(point);
299 if (inside != static_cast<bool>(solid.Contains_Loop(point))) {
300 continue;
301 }
302 const double mine = inside ? solid.DistFromInside_Loop(point, direction, TGeoShape::Big())
303 : solid.DistFromOutside_Loop(point, direction, TGeoShape::Big());
304 const double theirs = inside ? reference.DistFromInside(point, direction, 3, TGeoShape::Big(), nullptr)
305 : reference.DistFromOutside(point, direction, 3, TGeoShape::Big(), nullptr);
306 if (theirs >= TGeoShape::Big()) {
307 BOOST_REQUIRE_GE(mine, TGeoShape::Big());
308 } else {
309 BOOST_REQUIRE_SMALL(mine - theirs, 1.e-8);
310 }
311 }
312}
313
314BOOST_AUTO_TEST_CASE(a_ray_leaving_one_cell_into_a_touching_one_does_not_stop_between_them)
315{
316 // two unit boxes sharing the face at x = 1: the union's DistFromInside from the origin along +x
317 // is 3, not 1. This is why DistFromInside needs the union across cells and not one cell's exit.
318 O2FlatCSG solid("touching");
319 addBoxCell(solid, 1., 1., 1.);
320 const int first = solid.GetNhalfspaces();
321 const double planes[6][2][3] = {{{1., 0., 0.}, {3., 0., 0.}},
322 {{-1., 0., 0.}, {1., 0., 0.}},
323 {{0., 1., 0.}, {0., 1., 0.}},
324 {{0., -1., 0.}, {0., -1., 0.}},
325 {{0., 0., 1.}, {0., 0., 1.}},
326 {{0., 0., -1.}, {0., 0., -1.}}};
327 for (const auto& plane : planes) {
328 double coeff[10];
329 planeQuadric(plane[0], plane[1], coeff);
330 solid.AddQuadric(1., coeff);
331 }
332 solid.AddCell(first, 6, 8.);
333
334 const double origin[3] = {0., 0., 0.};
335 const double dir[3] = {1., 0., 0.};
336 BOOST_CHECK_SMALL(solid.DistFromInside_Loop(origin, dir, TGeoShape::Big()) - 3., 1.e-12);
337}
338
339BOOST_AUTO_TEST_CASE(tangential_ray_on_a_cylinder_from_a_point_on_its_surface_has_no_nan_root)
340{
341 // a ray tangential to a cylinder, starting exactly on its surface, has beta == 0 and gamma == 0
342 // together in HalfspaceRoots' quadratic -- the q == 0 case that used to divide 0./0. into a
343 // NaN second root instead of recognising the single double root at t = 0
344 O2FlatCSG solid("tangent_ray");
345 double coeff[10];
346 zCylinderQuadric(5., coeff);
347 solid.AddQuadric(1., coeff);
348 const auto& cylinder = solid.GetHalfspace(0);
349
350 const double origin[3] = {5., 0., 0.};
351 const double dir[3] = {0., 1., 0.};
352 double roots[4];
353 const int found = O2FlatCSG::HalfspaceRoots(cylinder, origin, dir, roots);
354
355 BOOST_REQUIRE_EQUAL(found, 1);
356 BOOST_CHECK(std::isfinite(roots[0]));
357 BOOST_CHECK_SMALL(roots[0], 1.e-12);
358
359 // the twin: an independent check that the reported root really is one, by plugging it back
360 // into the surface equation directly rather than trusting the root-finder's own algebra
361 const double hit[3] = {origin[0] + roots[0] * dir[0], origin[1] + roots[0] * dir[1],
362 origin[2] + roots[0] * dir[2]};
363 BOOST_CHECK_SMALL(O2FlatCSG::EvalHalfspace(cylinder, hit), 1.e-9);
364}
365
366BOOST_AUTO_TEST_CASE(torus_contains_and_distances_match_TGeoTorus)
367{
368 // a full torus, R = 10, r = 3, about z -- one cell of one halfspace
369 O2FlatCSG solid("torus");
370 const double centre[3] = {0., 0., 0.};
371 const double axis[3] = {0., 0., 1.};
372 solid.AddTorus(1., centre, axis, 10., 3.);
373 solid.AddCell(0, 1, 2. * TMath::Pi() * TMath::Pi() * 10. * 9.);
374
375 TGeoTorus reference(10., 0., 3.);
376 Rng rng(31415ULL);
377 int scoredPoints = 0;
378 for (int trial = 0; trial < 20000; ++trial) {
379 const double point[3] = {rng.uniform(-15., 15.), rng.uniform(-15., 15.), rng.uniform(-5., 5.)};
380 const double radial = std::hypot(point[0], point[1]);
381 const double distance = std::hypot(radial - 10., point[2]) - 3.;
382 if (std::abs(distance) < 1.e-9) {
383 continue;
384 }
385 BOOST_REQUIRE_EQUAL(solid.Contains_Loop(point), reference.Contains(point));
386 ++scoredPoints;
387 }
388 BOOST_CHECK_GT(scoredPoints, 19000);
389
390 for (int trial = 0; trial < 20000; ++trial) {
391 double point[3] = {rng.uniform(-20., 20.), rng.uniform(-20., 20.), rng.uniform(-8., 8.)};
392 double dir[3];
393 double norm = 0.;
394 do {
395 for (int index = 0; index < 3; ++index) {
396 dir[index] = rng.uniform(-1., 1.);
397 }
398 norm = std::sqrt(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]);
399 } while (norm < 1.e-3);
400 for (int index = 0; index < 3; ++index) {
401 dir[index] /= norm;
402 }
403 const bool inside = reference.Contains(point);
404 if (inside != static_cast<bool>(solid.Contains_Loop(point))) {
405 continue;
406 }
407 const double mine = inside ? solid.DistFromInside_Loop(point, dir, TGeoShape::Big())
408 : solid.DistFromOutside_Loop(point, dir, TGeoShape::Big());
409 const double theirs = inside ? reference.DistFromInside(point, dir, 3, TGeoShape::Big(), nullptr)
410 : reference.DistFromOutside(point, dir, 3, TGeoShape::Big(), nullptr);
411 if (theirs >= TGeoShape::Big()) {
412 BOOST_REQUIRE_GE(mine, TGeoShape::Big());
413 } else {
414 // the quartic is the looser of the two solvers; 1e-6 cm on a 10 cm torus
415 BOOST_REQUIRE_SMALL(mine - theirs, 1.e-6);
416 }
417 }
418}
419
420BOOST_AUTO_TEST_CASE(a_tilted_torus_is_the_same_solid_as_an_upright_one_rotated)
421{
422 // the frame handling is where a torus block goes wrong silently, so it gets its own case
423 const double axis[3] = {0., 1. / std::sqrt(2.), 1. / std::sqrt(2.)};
424 const double centre[3] = {1., 2., 3.};
425 O2FlatCSG solid("tilted_torus");
426 solid.AddTorus(1., centre, axis, 8., 2.);
427 solid.AddCell(0, 1, 0.);
428
429 Rng rng(2718ULL);
430 for (int trial = 0; trial < 20000; ++trial) {
431 const double point[3] = {rng.uniform(-14., 16.), rng.uniform(-13., 17.), rng.uniform(-12., 18.)};
432 // the closed-form signed distance is the reference: sqrt((rho - R)^2 + z^2) - r
433 const double offset[3] = {point[0] - centre[0], point[1] - centre[1], point[2] - centre[2]};
434 const double along = offset[0] * axis[0] + offset[1] * axis[1] + offset[2] * axis[2];
435 double radialVec[3];
436 for (int index = 0; index < 3; ++index) {
437 radialVec[index] = offset[index] - along * axis[index];
438 }
439 const double rho = std::sqrt(radialVec[0] * radialVec[0] + radialVec[1] * radialVec[1] +
440 radialVec[2] * radialVec[2]);
441 const double signedDistance = std::hypot(rho - 8., along) - 2.;
442 if (std::abs(signedDistance) < 1.e-9) {
443 continue;
444 }
445 BOOST_REQUIRE_EQUAL(static_cast<bool>(solid.Contains_Loop(point)), signedDistance < 0.);
446 }
447
448 // Contains_Loop only exercises EvalHalfspace's frame decomposition; HalfspaceRoots has its own,
449 // separate one (the pz/dz/pPerp/dPerp block), and the upright case never gives it a non-z axis
450 // to get wrong. Compare distances against TGeoTorus by carrying a local (upright, origin-
451 // centred) point and direction alongside a world one related by the same rotation that carries
452 // the local z axis onto `axis`, so the reference and the shape describe the same solid.
453 const double s = 1. / std::sqrt(2.);
454 // rotation about the world x axis that sends local (0,0,1) to (0, s, s) == axis
455 auto rotateToWorld = [s](const double local[3], double world[3]) {
456 world[0] = local[0];
457 world[1] = s * local[1] + s * local[2];
458 world[2] = -s * local[1] + s * local[2];
459 };
460
461 TGeoTorus reference(8., 0., 2.);
462 for (int trial = 0; trial < 20000; ++trial) {
463 double localPoint[3] = {rng.uniform(-20., 20.), rng.uniform(-20., 20.), rng.uniform(-8., 8.)};
464 double localDir[3];
465 double norm = 0.;
466 do {
467 for (int index = 0; index < 3; ++index) {
468 localDir[index] = rng.uniform(-1., 1.);
469 }
470 norm = std::sqrt(localDir[0] * localDir[0] + localDir[1] * localDir[1] + localDir[2] * localDir[2]);
471 } while (norm < 1.e-3);
472 for (int index = 0; index < 3; ++index) {
473 localDir[index] /= norm;
474 }
475 double worldPoint[3];
476 double worldDir[3];
477 rotateToWorld(localPoint, worldPoint);
478 rotateToWorld(localDir, worldDir);
479 for (int index = 0; index < 3; ++index) {
480 worldPoint[index] += centre[index];
481 }
482
483 const bool inside = reference.Contains(localPoint);
484 if (inside != static_cast<bool>(solid.Contains_Loop(worldPoint))) {
485 continue;
486 }
487 const double mine = inside ? solid.DistFromInside_Loop(worldPoint, worldDir, TGeoShape::Big())
488 : solid.DistFromOutside_Loop(worldPoint, worldDir, TGeoShape::Big());
489 const double theirs = inside ? reference.DistFromInside(localPoint, localDir, 3, TGeoShape::Big(), nullptr)
490 : reference.DistFromOutside(localPoint, localDir, 3, TGeoShape::Big(), nullptr);
491 if (theirs >= TGeoShape::Big()) {
492 BOOST_REQUIRE_GE(mine, TGeoShape::Big());
493 } else {
494 BOOST_REQUIRE_SMALL(mine - theirs, 1.e-6);
495 }
496 }
497}
498
499BOOST_AUTO_TEST_CASE(the_range_bound_encloses_the_sampled_range)
500{
501 // the bound must be an ENCLOSURE: over-wide is safe, under-wide is a wrong solid
502 O2FlatCSG solid("range");
503 double coeff[10];
504 zCylinderQuadric(5., coeff);
505 const int cylinder = solid.AddQuadric(1., coeff);
506 const double normal[3] = {0., 0., 1.};
507 const double through[3] = {0., 0., 2.};
508 planeQuadric(normal, through, coeff);
509 const int plane = solid.AddQuadric(-1., coeff);
510 const double centre[3] = {1., 0., 0.};
511 const double axis[3] = {0., 0., 1.};
512 const int torus = solid.AddTorus(1., centre, axis, 7., 2.);
513 tiltedCylinderQuadric(5., coeff);
514 const int tilted = solid.AddQuadric(1., coeff);
515
516 Rng rng(555ULL);
517 const std::vector<int> halfspaces = {cylinder, plane, torus, tilted};
518
519 auto checkBox = [&](const double* lo, const double* hi) {
520 for (int which : halfspaces) {
521 double rangeLo = 0.;
522 double rangeHi = 0.;
523 O2FlatCSG::HalfspaceRange(solid.GetHalfspace(which), lo, hi, rangeLo, rangeHi);
524 BOOST_REQUIRE_LE(rangeLo, rangeHi);
525
526 auto checkPoint = [&](const double point[3]) {
527 const double value = O2FlatCSG::EvalHalfspace(solid.GetHalfspace(which), point);
528 BOOST_REQUIRE_GE(value, rangeLo - 1.e-9);
529 BOOST_REQUIRE_LE(value, rangeHi + 1.e-9);
530 };
531
532 // deterministic coverage of the box's extremities: a plane's bound is tight exactly AT a
533 // corner, so uniform interior sampling has probability zero of ever landing where a
534 // slightly under-wide bound would actually be caught
535 for (int cx : {0, 1}) {
536 for (int cy : {0, 1}) {
537 for (int cz : {0, 1}) {
538 const double corner[3] = {cx ? hi[0] : lo[0], cy ? hi[1] : lo[1], cz ? hi[2] : lo[2]};
539 checkPoint(corner);
540 }
541 }
542 }
543 const double mid[3] = {0.5 * (lo[0] + hi[0]), 0.5 * (lo[1] + hi[1]), 0.5 * (lo[2] + hi[2])};
544 for (int faceAxis = 0; faceAxis < 3; ++faceAxis) {
545 for (int side : {0, 1}) {
546 double face[3] = {mid[0], mid[1], mid[2]};
547 face[faceAxis] = side ? hi[faceAxis] : lo[faceAxis];
548 checkPoint(face);
549 }
550 }
551 for (int edgeAxis = 0; edgeAxis < 3; ++edgeAxis) {
552 const int other1 = (edgeAxis + 1) % 3;
553 const int other2 = (edgeAxis + 2) % 3;
554 for (int s1 : {0, 1}) {
555 for (int s2 : {0, 1}) {
556 double edge[3];
557 edge[edgeAxis] = mid[edgeAxis];
558 edge[other1] = s1 ? hi[other1] : lo[other1];
559 edge[other2] = s2 ? hi[other2] : lo[other2];
560 checkPoint(edge);
561 }
562 }
563 }
564
565 // plus random interior samples, as before
566 for (int sample = 0; sample < 200; ++sample) {
567 const double point[3] = {rng.uniform(lo[0], hi[0]), rng.uniform(lo[1], hi[1]),
568 rng.uniform(lo[2], hi[2])};
569 checkPoint(point);
570 }
571 }
572 };
573
574 for (int trial = 0; trial < 3000; ++trial) {
575 double lo[3];
576 double hi[3];
577 for (int index = 0; index < 3; ++index) {
578 const double a = rng.uniform(-12., 12.);
579 const double b = a + rng.uniform(0.01, 6.);
580 lo[index] = a;
581 hi[index] = b;
582 }
583 checkBox(lo, hi);
584 }
585
586 // extreme-aspect-ratio boxes -- one axis ~0.01 wide, another ~24 -- outside the size range the
587 // random trials above ever draw (at most 6 wide per axis)
588 const double extreme[4][3][2] = {
589 {{-0.005, 0.005}, {-12., 12.}, {-0.5, 0.5}},
590 {{-12., 12.}, {-0.005, 0.005}, {3., 27.}},
591 {{2., 2.01}, {-1., 1.}, {-12., 12.}},
592 {{-24., 0.}, {5., 5.01}, {-3., 3.}},
593 };
594 for (const auto& box : extreme) {
595 const double lo[3] = {box[0][0], box[1][0], box[2][0]};
596 const double hi[3] = {box[0][1], box[1][1], box[2][1]};
597 checkBox(lo, hi);
598 }
599}
600
601BOOST_AUTO_TEST_CASE(the_boxes_cover_the_solid_and_their_active_lists_are_sound)
602{
603 // rmin = 2, rmax = 5, dz = 7 again, so there is a bore for the boxes to carve around
604 O2FlatCSG solid("boxes");
605 double coeff[10];
606 zCylinderQuadric(5., coeff);
607 solid.AddQuadric(1., coeff);
608 zCylinderQuadric(2., coeff);
609 solid.AddQuadric(-1., coeff);
610 const double up[3] = {0., 0., 1.};
611 const double down[3] = {0., 0., -1.};
612 const double top[3] = {0., 0., 7.};
613 const double bottom[3] = {0., 0., -7.};
614 planeQuadric(up, top, coeff);
615 solid.AddQuadric(1., coeff);
616 planeQuadric(down, bottom, coeff);
617 solid.AddQuadric(1., coeff);
618 solid.AddCell(0, 4, 0.);
619 const double lo[3] = {-5., -5., -7.};
620 const double hi[3] = {5., 5., 7.};
621 solid.SetCellBBox(0, lo, hi);
622 solid.CloseShape();
623
624 BOOST_CHECK_GT(solid.GetNboxes(), 1);
625
626 Rng rng(8080ULL);
627 int insideSamples = 0;
628 for (int trial = 0; trial < 50000; ++trial) {
629 const double point[3] = {rng.uniform(-6., 6.), rng.uniform(-6., 6.), rng.uniform(-8., 8.)};
630 if (!solid.Contains_Loop(point)) {
631 continue;
632 }
633 ++insideSamples;
634 // COVERAGE: every point of the solid is in some box
635 bool covered = false;
636 for (int index = 0; index < solid.GetNboxes() && !covered; ++index) {
637 const auto& box = solid.GetBox(index);
638 covered = point[0] >= box.min[0] && point[0] <= box.max[0] && point[1] >= box.min[1] &&
639 point[1] <= box.max[1] && point[2] >= box.min[2] && point[2] <= box.max[2];
640 }
641 BOOST_REQUIRE(covered);
642 }
643 BOOST_CHECK_GT(insideSamples, 5000);
644
645 // SOUNDNESS of the active lists: in every box, the active list alone decides membership
646 for (int index = 0; index < solid.GetNboxes(); ++index) {
647 const auto& box = solid.GetBox(index);
648 for (int sample = 0; sample < 200; ++sample) {
649 const double point[3] = {rng.uniform(box.min[0], box.max[0]),
650 rng.uniform(box.min[1], box.max[1]),
651 rng.uniform(box.min[2], box.max[2])};
652 bool byActive = true;
653 for (int slot = 0; slot < box.nActive && byActive; ++slot) {
654 byActive = O2FlatCSG::EvalHalfspace(
655 solid.GetHalfspace(solid.GetActive(box.firstActive + slot)), point) <= 0.;
656 }
657 BOOST_REQUIRE_EQUAL(byActive, solid.CellContains(box.cell, point));
658 }
659 }
660}
661
662BOOST_AUTO_TEST_CASE(a_box_wholly_inside_a_cell_carries_no_active_halfspaces)
663{
664 O2FlatCSG solid("solid_boxes");
665 addBoxCell(solid, 4., 4., 4.);
666 const double lo[3] = {-4., -4., -4.};
667 const double hi[3] = {4., 4., 4.};
668 solid.SetCellBBox(0, lo, hi);
669 // Both knobs are pinned, not defaulted: this case is about what the subdivision CAN produce,
670 // and the shipped defaults are chosen for query cost, which is
671 // a different question. Six levels on a cube is a 4 x 4 x 4 grid, whose innermost eight boxes
672 // touch no face.
673 solid.SetSplitDepth(6);
674 solid.SetMinBoxFraction(0.01);
675 solid.CloseShape();
676 // a box has six planes and is convex, so subdivision must find interior boxes with an empty list
677 int solidBoxes = 0;
678 for (int index = 0; index < solid.GetNboxes(); ++index) {
679 if (solid.GetBox(index).nActive == 0) {
680 ++solidBoxes;
681 }
682 }
683 BOOST_CHECK_GT(solidBoxes, 0);
684}
685
686BOOST_AUTO_TEST_CASE(a_cell_without_a_bbox_fails_loudly_instead_of_vanishing)
687{
688 // cell 0 gets a box; cell 1 (a second, disjoint box) never does -- CloseShape must refuse to
689 // build a partial, silently-wrong solid rather than just drop cell 1
690 O2FlatCSG solid("missing_bbox");
691 addBoxCell(solid, 1., 1., 1.);
692 const int first = solid.GetNhalfspaces();
693 const double centre[3] = {10., 0., 0.};
694 for (int axis = 0; axis < 3; ++axis) {
695 for (int sense = -1; sense <= 1; sense += 2) {
696 double normal[3] = {0., 0., 0.};
697 double through[3] = {centre[0], centre[1], centre[2]};
698 normal[axis] = static_cast<double>(sense);
699 through[axis] += sense * 1.;
700 double coeff[10];
701 planeQuadric(normal, through, coeff);
702 solid.AddQuadric(1., coeff);
703 }
704 }
705 solid.AddCell(first, 6, 8.);
706
707 const double lo[3] = {-1., -1., -1.};
708 const double hi[3] = {1., 1., 1.};
709 solid.SetCellBBox(0, lo, hi); // cell 1's box is never set
710
711 solid.CloseShape();
712 BOOST_CHECK(!solid.IsClosed());
713 BOOST_CHECK_EQUAL(solid.GetNboxes(), 0);
714}
715
716BOOST_AUTO_TEST_CASE(an_inverted_cell_bbox_fails_loudly_instead_of_being_kept_as_solid)
717{
718 // a converter that swapped lo/hi arguments must not get a shape that quietly reports itself
719 // closed: an all-axes-inverted box never grows past SplitBox's longest = 0. initialiser, so it
720 // would otherwise be kept immediately with an active list computed from a negative-half-extent
721 // (hence invalid) range bound -- possibly nActive == 0, which downstream reads as solid material
722 O2FlatCSG solid("inverted_bbox");
723 addBoxCell(solid, 1., 1., 1.);
724 const double lo[3] = {-1., -1., -1.};
725 const double hi[3] = {1., 1., 1.};
726 solid.SetCellBBox(0, hi, lo); // lo/hi swapped
727
728 solid.CloseShape();
729 BOOST_CHECK(!solid.IsClosed());
730 BOOST_CHECK_EQUAL(solid.GetNboxes(), 0);
731}
732
733BOOST_AUTO_TEST_CASE(a_nan_cell_bbox_fails_loudly_instead_of_defeating_the_inverted_box_check)
734{
735 // a NaN passes every ordinary "hi < lo" comparison silently (every comparison with NaN is
736 // false), so it must be its own check rather than fall through the inverted-box test above --
737 // otherwise it would reach HalfspaceRange, produce a NaN range that fails both of SplitBox's
738 // drop tests, and get kept as a spurious box
739 O2FlatCSG solid("nan_bbox");
740 addBoxCell(solid, 1., 1., 1.);
741 const double nan = std::numeric_limits<double>::quiet_NaN();
742 const double lo[3] = {-1., -1., -1.};
743 const double hi[3] = {1., nan, 1.};
744 solid.SetCellBBox(0, lo, hi);
745
746 solid.CloseShape();
747 BOOST_CHECK(!solid.IsClosed());
748 BOOST_CHECK_EQUAL(solid.GetNboxes(), 0);
749}
750
751namespace
752{
764void buildBracket(O2FlatCSG& solid, double planeScale = 1.)
765{
766 double coeff[10];
767 const auto scaledPlane = [&](const double* normal, const double* through) {
768 planeQuadric(normal, through, coeff);
769 for (int index = 0; index < 10; ++index) {
770 coeff[index] *= planeScale;
771 }
772 };
773 // cell 0: the long arm, x in [-10, 10], y in [-1, 1], z in [-1, 1]
774 const double arm[6][2][3] = {{{1., 0., 0.}, {10., 0., 0.}},
775 {{-1., 0., 0.}, {-10., 0., 0.}},
776 {{0., 1., 0.}, {0., 1., 0.}},
777 {{0., -1., 0.}, {0., -1., 0.}},
778 {{0., 0., 1.}, {0., 0., 1.}},
779 {{0., 0., -1.}, {0., 0., -1.}}};
780 int first = solid.GetNhalfspaces();
781 for (const auto& plane : arm) {
782 scaledPlane(plane[0], plane[1]);
783 solid.AddQuadric(1., coeff);
784 }
785 solid.AddCell(first, 6, 8. * 10. * 1. * 1.);
786 const double armLo[3] = {-10., -1., -1.};
787 const double armHi[3] = {10., 1., 1.};
788 solid.SetCellBBox(0, armLo, armHi);
789
790 // cell 1: the upright, x in [8, 10], y in [-1, 1], z in [1, 12]
791 const double upright[6][2][3] = {{{1., 0., 0.}, {10., 0., 0.}},
792 {{-1., 0., 0.}, {8., 0., 0.}},
793 {{0., 1., 0.}, {0., 1., 0.}},
794 {{0., -1., 0.}, {0., -1., 0.}},
795 {{0., 0., 1.}, {0., 0., 12.}},
796 {{0., 0., -1.}, {0., 0., 1.}}};
797 first = solid.GetNhalfspaces();
798 for (const auto& plane : upright) {
799 scaledPlane(plane[0], plane[1]);
800 solid.AddQuadric(1., coeff);
801 }
802 solid.AddCell(first, 6, 2. * 2. * 11.);
803 const double uprightLo[3] = {8., -1., 1.};
804 const double uprightHi[3] = {10., 1., 12.};
805 solid.SetCellBBox(1, uprightLo, uprightHi);
806
807 // cell 2: a washer around z at x = -8 and z in [1, 3], with a bore -- a complemented cylinder,
808 // so non-convex, and clear of the arm so the bore is empty space
809 first = solid.GetNhalfspaces();
810 const double centreShift = -8.;
811 // outer cylinder about the axis through (-8, 0, *): translate by completing the square
812 const double outer[10] = {1., 0., 0., 1., 0., 0., -centreShift, 0., 0.,
813 centreShift * centreShift - 9.};
814 solid.AddQuadric(1., outer);
815 const double inner[10] = {1., 0., 0., 1., 0., 0., -centreShift, 0., 0.,
816 centreShift * centreShift - 1.};
817 solid.AddQuadric(-1., inner);
818 const double washer[2][2][3] = {{{0., 0., 1.}, {0., 0., 3.}}, {{0., 0., -1.}, {0., 0., 1.}}};
819 for (const auto& plane : washer) {
820 scaledPlane(plane[0], plane[1]);
821 solid.AddQuadric(1., coeff);
822 }
823 solid.AddCell(first, 4, TMath::Pi() * (9. - 1.) * 2.);
824 const double washerLo[3] = {-11., -3., 1.};
825 const double washerHi[3] = {-5., 3., 3.};
826 solid.SetCellBBox(2, washerLo, washerHi);
827}
828} // namespace
829
830BOOST_AUTO_TEST_CASE(the_accelerated_contains_is_bit_identical_to_its_twin)
831{
832 O2FlatCSG solid("bracket");
833 buildBracket(solid);
834 solid.CloseShape();
835 BOOST_CHECK_GT(solid.GetNboxes(), 3);
836 BOOST_CHECK_GT(solid.GetBVHMemory(), 0u);
837
838 Rng rng(123456ULL);
839 for (int trial = 0; trial < 200000; ++trial) {
840 const double point[3] = {rng.uniform(-13., 13.), rng.uniform(-5., 5.), rng.uniform(-3., 14.)};
841 BOOST_REQUIRE_EQUAL(solid.Contains(point), solid.Contains_Loop(point));
842 }
843}
844
845BOOST_AUTO_TEST_CASE(the_sampled_boundary_points_flip_containment)
846{
847 O2FlatCSG solid("bracket_points");
848 buildBracket(solid);
849 solid.CloseShape();
850 constexpr int kPoints = 4000;
851 std::vector<double> points(3 * kPoints, 0.);
852 BOOST_REQUIRE(solid.GetPointsOnSegments(kPoints, points.data()));
853 const double zAxis[3] = {0., 0., 1.};
854 for (int index = 0; index < kPoints; ++index) {
855 const double* point = &points[3 * index];
856 double normal[3] = {0., 0., 0.};
857 solid.ComputeNormal(point, zAxis, normal);
858 double below[3];
859 double above[3];
860 for (int axis = 0; axis < 3; ++axis) {
861 below[axis] = point[axis] - 1.e-6 * normal[axis];
862 above[axis] = point[axis] + 1.e-6 * normal[axis];
863 }
864 BOOST_TEST_CONTEXT("point = (" << point[0] << ", " << point[1] << ", " << point[2] << ")")
865 {
866 BOOST_CHECK_NE(solid.Contains(below), solid.Contains(above));
867 }
868 }
869}
870
871BOOST_AUTO_TEST_CASE(the_accelerated_distances_are_bit_identical_to_their_twins)
872{
873 O2FlatCSG solid("bracket_dist");
874 buildBracket(solid);
875 solid.CloseShape();
876
877 Rng rng(654321ULL);
878 for (int trial = 0; trial < 200000; ++trial) {
879 double point[3] = {rng.uniform(-16., 16.), rng.uniform(-8., 8.), rng.uniform(-6., 17.)};
880 double dir[3];
881 double norm = 0.;
882 do {
883 for (int index = 0; index < 3; ++index) {
884 dir[index] = rng.uniform(-1., 1.);
885 }
886 norm = std::sqrt(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]);
887 } while (norm < 1.e-3);
888 for (int index = 0; index < 3; ++index) {
889 dir[index] /= norm;
890 }
891 if (solid.Contains_Loop(point)) {
892 BOOST_REQUIRE_EQUAL(solid.DistFromInside(point, dir, 3, TGeoShape::Big(), nullptr),
893 solid.DistFromInside_Loop(point, dir, TGeoShape::Big()));
894 } else {
895 BOOST_REQUIRE_EQUAL(solid.DistFromOutside(point, dir, 3, TGeoShape::Big(), nullptr),
896 solid.DistFromOutside_Loop(point, dir, TGeoShape::Big()));
897 }
898 }
899}
900
901BOOST_AUTO_TEST_CASE(a_ray_along_the_long_arm_crosses_every_cell_it_should)
902{
903 // the case a per-box clip gets wrong if it forgets to clip: a ray running the length of the
904 // bracket passes through many boxes of the same cell, and must see ONE interval, not many
905 O2FlatCSG solid("bracket_long");
906 buildBracket(solid);
907 solid.CloseShape();
908 const double dir[3] = {1., 0., 0.};
909
910 // the entry, which one box decides on its own: nothing lies before the arm along z = 0
911 const double origin[3] = {-20., 0., 0.};
912 BOOST_CHECK_SMALL(solid.DistFromOutside(origin, dir, 3, TGeoShape::Big(), nullptr) - 10., 1.e-12);
913
914 // the exit, which sixteen boxes of cell 0 decide together: the arm is split along x into boxes
915 // 1.25 wide, so this is the cross-box join, and a traversal that forgot it would stop at the
916 // first box boundary
917 const double inArm[3] = {0., 0., 0.};
918 // inside the arm at the origin, the exit is x = 10 (the arm and the upright touch at x = 8..10
919 // only for z > 1, so along z = 0 the arm alone decides)
920 BOOST_CHECK_SMALL(solid.DistFromInside(inArm, dir, 3, TGeoShape::Big(), nullptr) - 10., 1.e-12);
921
922 // An ENTRY that needs the per-cell merge, which is otherwise hard to reach: the twin's rule
923 // takes the smallest entry over the intervals whose exit clears TGeoShape::Tolerance(), so a
924 // box boundary crossed within the tolerance of the origin cuts the real interval into a
925 // sub-tolerance stub the rule would throw away, and the answer would jump from the true entry
926 // to the box boundary. This ray starts 1e-11 outside the arm's y = 1 face and 2e-11 before its
927 // x = -8.75 box boundary, so both crossings sit inside the tolerance.
928 const double grazing[3] = {-8.75 - 2.e-11, 1. + 1.e-11, 0.5};
929 const double slant = 1. / std::sqrt(2.);
930 const double slantDir[3] = {slant, -slant, 0.};
931 const double entered = solid.DistFromOutside(grazing, slantDir, 3, TGeoShape::Big(), nullptr);
932 BOOST_CHECK_EQUAL(entered, solid.DistFromOutside_Loop(grazing, slantDir, TGeoShape::Big()));
933 // and it really is in the regime where a per-box rule would differ: below the tolerance, and
934 // strictly nearer than the x = -8.75 box boundary at 2e-11 * sqrt(2)
935 BOOST_CHECK_GT(entered, 0.);
936 BOOST_CHECK_LT(entered, TGeoShape::Tolerance());
937 BOOST_CHECK_LT(entered, 2.e-11 * std::sqrt(2.));
938}
939
940BOOST_AUTO_TEST_CASE(the_accelerated_distances_track_their_twins_when_a_plane_is_rescaled)
941{
942 // Bit identity between an accelerated query and its twin is a self-check discipline, not a
943 // physics requirement: a one-ulp difference in an exit distance is navigationally irrelevant.
944 // It is achievable only under the plane convention of design section 3.1, where a unit normal n
945 // is stored as 2b = n. There the slab bound (v - o_k) / d_k and the root -0.5*gamma/beta divide
946 // numerator and denominator each scaled by exactly one half, so the single IEEE division returns
947 // the same double for both, and a box face lying on a plane halfspace is crossed at one value.
948 //
949 // Rescaling every plane by a NON-POWER-OF-TWO -- 3 here, which is what an unnormalised carrier
950 // normal (3, 0, 0) would give -- describes exactly the same solid, but fl(1.5 * d_x) rounds, the
951 // root moves off the slab bound by an ulp, and the last bit is lost. Nothing else in the system
952 // notices, so this test pins the size of what is lost: the answers must still agree closely.
953 O2FlatCSG solid("bracket_scaled");
954 buildBracket(solid, 3.);
955 solid.CloseShape();
956 BOOST_REQUIRE(solid.IsClosed());
957
958 Rng rng(1357911ULL);
959 double worst = 0.;
960 for (int trial = 0; trial < 200000; ++trial) {
961 double point[3] = {rng.uniform(-16., 16.), rng.uniform(-8., 8.), rng.uniform(-6., 17.)};
962 double dir[3];
963 double norm = 0.;
964 do {
965 for (int index = 0; index < 3; ++index) {
966 dir[index] = rng.uniform(-1., 1.);
967 }
968 norm = std::sqrt(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]);
969 } while (norm < 1.e-3);
970 for (int index = 0; index < 3; ++index) {
971 dir[index] /= norm;
972 }
973 const bool inside = solid.Contains_Loop(point);
974 // Contains has no arithmetic of its own to lose, so it stays bit-identical under any scale
975 BOOST_REQUIRE_EQUAL(solid.Contains(point), inside);
976 const double accelerated =
977 inside ? solid.DistFromInside(point, dir, 3, TGeoShape::Big(), nullptr)
978 : solid.DistFromOutside(point, dir, 3, TGeoShape::Big(), nullptr);
979 const double twin = inside ? solid.DistFromInside_Loop(point, dir, TGeoShape::Big())
980 : solid.DistFromOutside_Loop(point, dir, TGeoShape::Big());
981 const double slack = std::abs(accelerated - twin);
982 worst = std::max(worst, slack);
983 // The bound is set just above what the rescale actually costs -- the run below measures
984 // 1.24e-14 -- so the assertion, and not only the message under it, is what pins the size of
985 // the loss. A looser bound would pass on a rescale that had broken something far larger.
986 BOOST_REQUIRE_LE(slack, 1.e-13 * std::max(1., std::abs(twin)));
987 }
988 BOOST_TEST_MESSAGE("largest accelerated-vs-twin gap under a x3 plane rescale: " << worst);
989 // The aggregate is deliberately looser than the relative assertion above: it is an absolute
990 // bound on a maximum over a sample, and FMA contraction or a different libm moves the last
991 // couple of ulps. 1e-12 still pins the size of the loss a thousand times tighter than the
992 // 1e-9 this test used to assert, without being a cross-platform tripwire.
993 BOOST_CHECK_LE(worst, 1.e-12);
994}
995
996BOOST_AUTO_TEST_CASE(a_shape_that_failed_to_close_still_answers_through_the_loop_twins)
997{
998 // CloseShape refuses an unset cell bbox and builds nothing, so there is no box array and no BVH.
999 // An accelerated query that walked the empty array would answer "no material anywhere" -- the
1000 // silent vanishing the refusal exists to prevent -- so all three must fall back to the twins.
1001 O2FlatCSG solid("bracket_unclosed");
1002 buildBracket(solid);
1003 // a fourth cell, a box at x in [12, 14], deliberately left without a bounding box
1004 const double extra[6][2][3] = {{{1., 0., 0.}, {14., 0., 0.}},
1005 {{-1., 0., 0.}, {12., 0., 0.}},
1006 {{0., 1., 0.}, {0., 1., 0.}},
1007 {{0., -1., 0.}, {0., -1., 0.}},
1008 {{0., 0., 1.}, {0., 0., 1.}},
1009 {{0., 0., -1.}, {0., 0., -1.}}};
1010 double coeff[10];
1011 const int first = solid.GetNhalfspaces();
1012 for (const auto& plane : extra) {
1013 planeQuadric(plane[0], plane[1], coeff);
1014 solid.AddQuadric(1., coeff);
1015 }
1016 solid.AddCell(first, 6, 2. * 2. * 2.);
1017
1018 solid.CloseShape();
1019 BOOST_REQUIRE(!solid.IsClosed());
1020 BOOST_CHECK_EQUAL(solid.GetNboxes(), 0);
1021 BOOST_CHECK_EQUAL(solid.GetBVHMemory(), 0u);
1022
1023 // material inside every one of the four cells is still found, and empty space is still empty
1024 const double inArm[3] = {0., 0., 0.};
1025 const double inUpright[3] = {9., 0., 6.};
1026 const double inWasher[3] = {-10.5, 0., 2.};
1027 const double inExtra[3] = {13., 0., 0.};
1028 // the washer's bore, which is empty space now that the washer sits above the arm
1029 const double inBore[3] = {-8., 0., 2.};
1030 const double outside[3] = {0., 0., 20.};
1031 BOOST_CHECK(solid.Contains(inArm));
1032 BOOST_CHECK(solid.Contains(inUpright));
1033 BOOST_CHECK(solid.Contains(inWasher));
1034 BOOST_CHECK(solid.Contains(inExtra));
1035 BOOST_CHECK(!solid.Contains(inBore));
1036 BOOST_CHECK(!solid.Contains(outside));
1037
1038 Rng rng(24680ULL);
1039 for (int trial = 0; trial < 20000; ++trial) {
1040 const double point[3] = {rng.uniform(-16., 16.), rng.uniform(-5., 5.), rng.uniform(-3., 14.)};
1041 BOOST_REQUIRE_EQUAL(solid.Contains(point), solid.Contains_Loop(point));
1042 double dir[3];
1043 double norm = 0.;
1044 do {
1045 for (int index = 0; index < 3; ++index) {
1046 dir[index] = rng.uniform(-1., 1.);
1047 }
1048 norm = std::sqrt(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]);
1049 } while (norm < 1.e-3);
1050 for (int index = 0; index < 3; ++index) {
1051 dir[index] /= norm;
1052 }
1053 const bool inside = solid.Contains_Loop(point);
1054 if (inside) {
1055 BOOST_REQUIRE_EQUAL(solid.DistFromInside(point, dir, 3, TGeoShape::Big(), nullptr),
1056 solid.DistFromInside_Loop(point, dir, TGeoShape::Big()));
1057 } else {
1058 BOOST_REQUIRE_EQUAL(solid.DistFromOutside(point, dir, 3, TGeoShape::Big(), nullptr),
1059 solid.DistFromOutside_Loop(point, dir, TGeoShape::Big()));
1060 }
1061 // Safety falls back to its twin exactly like the other three accelerated queries -- this was
1062 // asserted for Contains and the distances above but never extended to Safety
1063 BOOST_REQUIRE_EQUAL(solid.Safety(point, inside), solid.Safety_Loop(point, inside));
1064 }
1065}
1066
1067BOOST_AUTO_TEST_CASE(safety_is_sound_and_matches_its_twin)
1068{
1069 O2FlatCSG solid("bracket_safety");
1070 buildBracket(solid);
1071 solid.CloseShape();
1072
1073 Rng rng(24680ULL);
1074 for (int trial = 0; trial < 50000; ++trial) {
1075 double point[3] = {rng.uniform(-16., 16.), rng.uniform(-8., 8.), rng.uniform(-6., 17.)};
1076 const bool inside = solid.Contains_Loop(point);
1077 const double safety = solid.Safety(point, inside);
1078 BOOST_REQUIRE_GE(safety, 0.);
1079 BOOST_REQUIRE_EQUAL(safety, solid.Safety_Loop(point, inside));
1080
1081 // SOUNDNESS: no point within `safety` of `point` may have the opposite classification
1082 for (int probe = 0; probe < 40; ++probe) {
1083 double dir[3];
1084 double norm = 0.;
1085 do {
1086 for (int index = 0; index < 3; ++index) {
1087 dir[index] = rng.uniform(-1., 1.);
1088 }
1089 norm = std::sqrt(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]);
1090 } while (norm < 1.e-3);
1091 const double reach = safety * rng.uniform(0., 0.999) / norm;
1092 const double near[3] = {point[0] + reach * dir[0], point[1] + reach * dir[1],
1093 point[2] + reach * dir[2]};
1094 BOOST_REQUIRE_EQUAL(static_cast<bool>(solid.Contains_Loop(near)), inside);
1095 }
1096 }
1097}
1098
1099BOOST_AUTO_TEST_CASE(safety_is_sound_when_the_inside_bound_is_actually_nonzero)
1100{
1101 // At the class's default split depth, buildBracket's arm is so far from cubic (20 x 2 x 2) that
1102 // the depth cap fires before any leaf fully detaches from all six faces: EVERY box keeps
1103 // nActive != 0, so the inside branch's `nActive == 0` selection path -- the one piece of
1104 // `Safety` whose soundness rests on a structural invariant (design section 4.2's hard guarantee)
1105 // rather than an exact box-distance formula -- is never taken by the test above. Its probes are
1106 // then vacuous: with `safety == 0.`, `reach` is always `0.` too, so the "nearby" point IS the
1107 // query point and the soundness check is trivially true. A deeper split makes solid boxes exist
1108 // (see the fix-round measurement in the task report), which this case forces so the nActive == 0
1109 // path is genuinely exercised end to end, not just agreed upon by two implementations at zero.
1110 O2FlatCSG solid("bracket_safety_deep");
1111 buildBracket(solid);
1112 solid.SetSplitDepth(14);
1113 // The size floor has to come down with the depth cap, or it stops the split first: at the
1114 // shipped 0.05 the arm is thinner than one minimum box and no leaf ever detaches.
1115 solid.SetMinBoxFraction(0.002);
1116 solid.CloseShape();
1117
1118 bool sawPositiveInsideSafety = false;
1119 Rng rng(11235813ULL);
1120 for (int trial = 0; trial < 50000; ++trial) {
1121 double point[3] = {rng.uniform(-16., 16.), rng.uniform(-8., 8.), rng.uniform(-6., 17.)};
1122 const bool inside = solid.Contains_Loop(point);
1123 const double safety = solid.Safety(point, inside);
1124 BOOST_REQUIRE_GE(safety, 0.);
1125 BOOST_REQUIRE_EQUAL(safety, solid.Safety_Loop(point, inside));
1126 if (inside && safety > 0.) {
1127 sawPositiveInsideSafety = true;
1128 }
1129
1130 // the same soundness probes as above, now with genuine reach on at least some trials
1131 for (int probe = 0; probe < 40; ++probe) {
1132 double dir[3];
1133 double norm = 0.;
1134 do {
1135 for (int index = 0; index < 3; ++index) {
1136 dir[index] = rng.uniform(-1., 1.);
1137 }
1138 norm = std::sqrt(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]);
1139 } while (norm < 1.e-3);
1140 const double reach = safety * rng.uniform(0., 0.999) / norm;
1141 const double near[3] = {point[0] + reach * dir[0], point[1] + reach * dir[1],
1142 point[2] + reach * dir[2]};
1143 BOOST_REQUIRE_EQUAL(static_cast<bool>(solid.Contains_Loop(near)), inside);
1144 }
1145 }
1146 BOOST_REQUIRE(sawPositiveInsideSafety);
1147}
1148
1149BOOST_AUTO_TEST_CASE(capacity_is_the_sum_of_the_cell_volumes)
1150{
1151 O2FlatCSG solid("bracket_capacity");
1152 buildBracket(solid);
1153 solid.CloseShape();
1154 const double expected = 8. * 10. * 1. * 1. + 2. * 2. * 11. + TMath::Pi() * (9. - 1.) * 2.;
1155 BOOST_CHECK_SMALL(solid.Capacity() - expected, 1.e-12);
1156}
1157
1158BOOST_AUTO_TEST_CASE(the_bounding_box_is_tight_around_the_retained_boxes)
1159{
1160 // ComputeBBox is the union of the RETAINED boxes -- a subset of the union of the cell AABBs --
1161 // so exact GetDX/GetDY/GetDZ/GetOrigin values depend on subdivision details rather than on the
1162 // contract. Assert the two legs that matter for navigation correctness instead: the bounding
1163 // box holds the whole solid, and it does not overshoot past what the cells could possibly reach.
1164 O2FlatCSG solid("bracket_bbox");
1165 buildBracket(solid);
1166 solid.CloseShape();
1167
1168 // every point of the solid is inside the bounding box
1169 Rng rng(13579ULL);
1170 for (int trial = 0; trial < 50000; ++trial) {
1171 const double point[3] = {rng.uniform(-13., 13.), rng.uniform(-5., 5.), rng.uniform(-3., 14.)};
1172 if (solid.Contains_Loop(point)) {
1173 BOOST_REQUIRE(solid.TGeoBBox::Contains(point));
1174 }
1175 }
1176
1177 // the bounding box is contained in the union of the cell AABBs, which buildBracket fixes:
1178 // arm x in [-10, 10], y in [-1, 1], z in [-1, 1]; upright x in [8, 10], y in [-1, 1], z in
1179 // [1, 12]; washer x in [-11, -5], y in [-3, 3], z in [1, 3]
1180 const double cellLo[3][3] = {{-10., -1., -1.}, {8., -1., 1.}, {-11., -3., 1.}};
1181 const double cellHi[3][3] = {{10., 1., 1.}, {10., 1., 12.}, {-5., 3., 3.}};
1182 double unionLo[3] = {cellLo[0][0], cellLo[0][1], cellLo[0][2]};
1183 double unionHi[3] = {cellHi[0][0], cellHi[0][1], cellHi[0][2]};
1184 for (int cell = 1; cell < 3; ++cell) {
1185 for (int index = 0; index < 3; ++index) {
1186 unionLo[index] = std::min(unionLo[index], cellLo[cell][index]);
1187 unionHi[index] = std::max(unionHi[index], cellHi[cell][index]);
1188 }
1189 }
1190 const double* origin = solid.GetOrigin();
1191 for (int index = 0; index < 3; ++index) {
1192 const double dHalf = index == 0 ? solid.GetDX() : (index == 1 ? solid.GetDY() : solid.GetDZ());
1193 BOOST_CHECK_GE(origin[index] - dHalf, unionLo[index] - 1.e-9);
1194 BOOST_CHECK_LE(origin[index] + dHalf, unionHi[index] + 1.e-9);
1195 }
1196}
1197
1198BOOST_AUTO_TEST_CASE(the_normal_on_a_face_is_the_face_normal)
1199{
1200 O2FlatCSG solid("box_normal");
1201 addBoxCell(solid, 3., 4., 5.);
1202 const double lo[3] = {-3., -4., -5.};
1203 const double hi[3] = {3., 4., 5.};
1204 solid.SetCellBBox(0, lo, hi);
1205 solid.CloseShape();
1206
1207 const double onFace[3] = {3., 1., 1.};
1208 const double dir[3] = {1., 0., 0.};
1209 double normal[3] = {0., 0., 0.};
1210 solid.ComputeNormal(onFace, dir, normal);
1211 BOOST_CHECK_SMALL(normal[0] - 1., 1.e-12);
1212 BOOST_CHECK_SMALL(normal[1], 1.e-12);
1213 BOOST_CHECK_SMALL(normal[2], 1.e-12);
1214}
1215
1216BOOST_AUTO_TEST_CASE(the_normal_selection_is_scale_invariant_across_cells)
1217{
1218 // Fix round 1: |EvalHalfspace| alone is not a distance -- its gain per unit distance is 1 for a
1219 // unit plane but ~2R for a cylinder of radius R, so a naive argmin over |f| can pick a distant
1220 // plane over the surface the point is actually on. Two cells make the point concrete: cell 0 is
1221 // a cylinder of radius 100 about z, cell 1 a single plane at z = 0.05. The test point sits
1222 // 0.0005 from the cylinder wall (radially) and 0.05 from the plane -- the cylinder is the true
1223 // nearest surface by two orders of magnitude, but |f_cylinder| ~= 0.1 > |f_plane| = 0.05, so the
1224 // unscaled rule would have picked the plane and returned (0, 0, 1) instead of the correct
1225 // (1, 0, 0). Deliberately not closed: with CloseShape run, the box-restriction half of the fix
1226 // alone would make this pass trivially (the point's own box never sees the other cell's plane),
1227 // so this exercises ComputeNormal's cross-cell fallback scan, where only the |f| / |grad f|
1228 // fix -- not the box restriction -- can be what saves it.
1229 O2FlatCSG solid("scale_invariance");
1230 double coeff[10];
1231 zCylinderQuadric(100., coeff);
1232 solid.AddQuadric(1., coeff);
1233 solid.AddCell(0, 1, 0.);
1234
1235 const double planeNormal[3] = {0., 0., 1.};
1236 const double planeThrough[3] = {0., 0., 0.05};
1237 planeQuadric(planeNormal, planeThrough, coeff);
1238 const int planeFirst = solid.GetNhalfspaces();
1239 solid.AddQuadric(1., coeff);
1240 solid.AddCell(planeFirst, 1, 0.);
1241
1242 BOOST_REQUIRE(!solid.IsClosed());
1243
1244 const double point[3] = {99.9995, 0., 0.};
1245 const double dir[3] = {1., 0., 0.};
1246 double normal[3] = {0., 0., 0.};
1247 solid.ComputeNormal(point, dir, normal);
1248 BOOST_CHECK_SMALL(normal[0] - 1., 1.e-9);
1249 BOOST_CHECK_SMALL(normal[1], 1.e-9);
1250 BOOST_CHECK_SMALL(normal[2], 1.e-9);
1251}
1252
1253BOOST_AUTO_TEST_CASE(a_zero_extent_cell_bbox_does_not_burn_the_whole_cubify_budget)
1254{
1255 // Fix round 2: a cell bbox with a genuinely zero extent on one axis passes CloseShape's
1256 // validation (it rejects only unset, inverted or non-finite boxes, not degenerate-but-flat
1257 // ones). Without SplitBox's `shortest` floor at `minSize`, that axis's extent stays pinned at
1258 // zero forever (it is never the longest, so never split), making `longest > 2 * shortest`
1259 // permanently true and spending the ENTIRE per-path cubify ceiling on a cell a depth-only rule
1260 // would have resolved in a handful of splits -- roughly `2^kMaxCubifySplits` leaves along every
1261 // branch instead. A 100 x 100 x 0 slab, subdivided down to the default minSize floor, needs on
1262 // the order of a dozen splits total once x and y are treated as the only axes that matter; this
1263 // asserts the box count stays in that regime rather than climbing towards the ceiling.
1264 O2FlatCSG solid("flat_cell");
1265 double coeff[10];
1266 const double planes[6][2][3] = {
1267 {{1., 0., 0.}, {50., 0., 0.}}, {{-1., 0., 0.}, {-50., 0., 0.}}, {{0., 1., 0.}, {0., 50., 0.}}, {{0., -1., 0.}, {0., -50., 0.}}, {{0., 0., 1.}, {0., 0., 0.}}, {{0., 0., -1.}, {0., 0., 0.}}};
1268 for (const auto& plane : planes) {
1269 planeQuadric(plane[0], plane[1], coeff);
1270 solid.AddQuadric(1., coeff);
1271 }
1272 solid.AddCell(0, 6, 0.);
1273 const double lo[3] = {-50., -50., 0.};
1274 const double hi[3] = {50., 50., 0.};
1275 solid.SetCellBBox(0, lo, hi);
1276 solid.CloseShape();
1277
1278 BOOST_REQUIRE(solid.IsClosed());
1279 // measured 272 boxes with the guard in place; a generous margin above that, and two orders of
1280 // magnitude below what hitting the per-path ceiling on every branch would produce
1281 BOOST_CHECK_LT(solid.GetNboxes(), 600);
1282}
1283
1284BOOST_AUTO_TEST_CASE(a_sidecar_round_trip_reproduces_the_solid)
1285{
1286 O2FlatCSG original("bracket_io");
1287 buildBracket(original);
1288 original.CloseShape();
1289
1290 const std::string path = "testFlatCSG_roundtrip.bin";
1291 BOOST_REQUIRE(o2::cad::WriteFlatCSG(path, original)); // test-only writer
1292
1293 O2FlatCSG loaded("bracket_io_loaded");
1294 BOOST_REQUIRE(o2::cad::LoadFlatCSG(path, loaded));
1295 loaded.CloseShape();
1296
1297 BOOST_CHECK_EQUAL(loaded.GetNhalfspaces(), original.GetNhalfspaces());
1298 BOOST_CHECK_EQUAL(loaded.GetNcells(), original.GetNcells());
1299 BOOST_CHECK_EQUAL(loaded.GetNboxes(), original.GetNboxes());
1300 BOOST_CHECK_EQUAL(loaded.Capacity(), original.Capacity());
1301
1302 Rng rng(97531ULL);
1303 for (int trial = 0; trial < 100000; ++trial) {
1304 const double point[3] = {rng.uniform(-13., 13.), rng.uniform(-5., 5.), rng.uniform(-3., 14.)};
1305 BOOST_REQUIRE_EQUAL(loaded.Contains(point), original.Contains(point));
1306 }
1307 std::filesystem::remove(path);
1308}
1309
1310BOOST_AUTO_TEST_CASE(writing_an_unclosed_shape_is_refused)
1311{
1312 // GetCellBBox reads back zeros for a cell whose box was never set -- a finite, non-inverted box
1313 // that would otherwise pass CloseShape's own validation on reload, silently shipping a
1314 // degenerate point-box for that cell. WriteFlatCSG refuses before that invariant can ever reach
1315 // a file: no CloseShape() call at all, and a cell missing a bbox (CloseShape() refused).
1316 const std::string path = "testFlatCSG_unclosed.bin";
1317
1318 O2FlatCSG neverClosed("bracket_never_closed");
1319 buildBracket(neverClosed);
1320 BOOST_REQUIRE(!neverClosed.IsClosed());
1321 BOOST_CHECK(!o2::cad::WriteFlatCSG(path, neverClosed));
1322 BOOST_CHECK(!std::filesystem::exists(path));
1323
1324 O2FlatCSG refused("bracket_refused_close");
1325 double coeff[10];
1326 const double plane[2][3] = {{1., 0., 0.}, {0., 0., 0.}};
1327 planeQuadric(plane[0], plane[1], coeff);
1328 refused.AddQuadric(1., coeff);
1329 refused.AddCell(0, 1, 0.); // no SetCellBBox for this cell -- CloseShape must refuse
1330 refused.CloseShape();
1331 BOOST_REQUIRE(!refused.IsClosed());
1333 BOOST_CHECK(!std::filesystem::exists(path));
1334}
1335
1336BOOST_AUTO_TEST_CASE(a_truncated_sidecar_is_refused_rather_than_half_loaded)
1337{
1338 O2FlatCSG original("bracket_trunc");
1339 buildBracket(original);
1340 original.CloseShape();
1341 const std::string path = "testFlatCSG_truncated.bin";
1342 BOOST_REQUIRE(o2::cad::WriteFlatCSG(path, original));
1343 std::filesystem::resize_file(path, std::filesystem::file_size(path) - 17);
1344
1345 O2FlatCSG loaded("bracket_trunc_loaded");
1347 std::filesystem::remove(path);
1348}
1349
1350BOOST_AUTO_TEST_CASE(the_shape_survives_a_ROOT_file_without_its_sidecar)
1351{
1352 O2FlatCSG original("bracket_root");
1353 buildBracket(original);
1354 original.CloseShape();
1355
1356 const std::string path = "testFlatCSG_shape.root";
1357 {
1358 TFile file(path.c_str(), "RECREATE");
1359 file.WriteObject(&original, "shape");
1360 }
1361 O2FlatCSG* restored = nullptr;
1362 {
1363 TFile file(path.c_str(), "READ");
1364 file.GetObject("shape", restored);
1365 }
1366 BOOST_REQUIRE(restored != nullptr);
1367 restored->CloseShape(); // the BVH is not streamed; it is rebuilt
1368
1369 Rng rng(11223ULL);
1370 for (int trial = 0; trial < 100000; ++trial) {
1371 const double point[3] = {rng.uniform(-13., 13.), rng.uniform(-5., 5.), rng.uniform(-3., 14.)};
1372 BOOST_REQUIRE_EQUAL(restored->Contains(point), original.Contains(point));
1373 }
1374 std::filesystem::remove(path);
1375}
1376
1377namespace
1378{
1380void addBoxAsCell(O2FlatCSG& solid, const double* lo, const double* hi)
1381{
1382 const int first = solid.GetNhalfspaces();
1383 double coeff[10];
1384 for (int axis = 0; axis < 3; ++axis) {
1385 for (int sense = -1; sense <= 1; sense += 2) {
1386 double normal[3] = {0., 0., 0.};
1387 double through[3] = {0., 0., 0.};
1388 normal[axis] = static_cast<double>(sense);
1389 through[axis] = sense > 0 ? hi[axis] : lo[axis];
1390 planeQuadric(normal, through, coeff);
1391 solid.AddQuadric(1., coeff);
1392 }
1393 }
1394 const int cell = solid.AddCell(first, 6, (hi[0] - lo[0]) * (hi[1] - lo[1]) * (hi[2] - lo[2]));
1395 solid.SetCellBBox(cell, lo, hi);
1396}
1397
1401void buildStaggeredChain(O2FlatCSG& solid)
1402{
1403 const double nearLo[3] = {0., -1., -1.};
1404 const double nearHi[3] = {1., 20., 1.};
1405 const double farLo[3] = {5., -1., -1.};
1406 const double farHi[3] = {6., 20., 1.};
1407 const double middleLo[3] = {1., -1., -1.};
1408 const double middleHi[3] = {5., 1., 1.};
1409 addBoxAsCell(solid, nearLo, nearHi);
1410 addBoxAsCell(solid, farLo, farHi);
1411 addBoxAsCell(solid, middleLo, middleHi);
1412 solid.CloseShape();
1413}
1414} // namespace
1415
1416BOOST_AUTO_TEST_CASE(a_far_box_that_extends_the_union_is_recovered_by_the_unpruned_retry)
1417{
1418 O2FlatCSG solid("staggered");
1419 buildStaggeredChain(solid);
1420
1421 // along the chain: the answer is the far cell's exit at x = 6, which the pruned traversal can
1422 // only reach through the middle cell it sees last
1423 const double point[3] = {0.5, 0., 0.};
1424 const double dir[3] = {1., 0., 0.};
1425 O2FlatCSG::ResetUnprunedRetryCounter();
1426 const double distance = solid.DistFromInside(point, dir, 3, TGeoShape::Big(), nullptr);
1427 BOOST_CHECK_EQUAL(distance, solid.DistFromInside_Loop(point, dir, TGeoShape::Big()));
1428 BOOST_CHECK_CLOSE(distance, 5.5, 1.e-9);
1429 BOOST_CHECK_GT(O2FlatCSG::GetUnprunedRetryCount(), 0);
1430}
1431
1432BOOST_AUTO_TEST_CASE(the_pruned_DistFromInside_is_bit_identical_to_its_twin_on_the_staggered_chain)
1433{
1434 O2FlatCSG solid("staggered_random");
1435 buildStaggeredChain(solid);
1436
1437 Rng rng(97531ULL);
1438 int inside = 0;
1439 for (int trial = 0; trial < 100000; ++trial) {
1440 double point[3] = {rng.uniform(-1., 7.), rng.uniform(-2., 21.), rng.uniform(-2., 2.)};
1441 if (!solid.Contains_Loop(point)) {
1442 continue;
1443 }
1444 double dir[3];
1445 double norm = 0.;
1446 do {
1447 for (int index = 0; index < 3; ++index) {
1448 dir[index] = rng.uniform(-1., 1.);
1449 }
1450 norm = std::sqrt(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]);
1451 } while (norm < 1.e-3);
1452 for (int index = 0; index < 3; ++index) {
1453 dir[index] /= norm;
1454 }
1455 ++inside;
1456 BOOST_REQUIRE_EQUAL(solid.DistFromInside(point, dir, 3, TGeoShape::Big(), nullptr),
1457 solid.DistFromInside_Loop(point, dir, TGeoShape::Big()));
1458 // a finite step must answer as the twin does with the same step
1459 BOOST_REQUIRE_EQUAL(solid.DistFromInside(point, dir, 3, 2., nullptr),
1460 solid.DistFromInside_Loop(point, dir, 2.));
1461 }
1462 BOOST_CHECK_GT(inside, 1000);
1463}
header::DataOrigin origin
configStandalone Light OpenGL display display_light Apply smoothing to points Enable Z buffer Highlight links Highlight seeds Highlight sector tracks Highlight final tracks Show individual Show related Highlight grid Show only individual collision Color clusters belonging to track objects Set line Enable drawing TPC data Enable drawing TOF data Enable drawing magnetic field Set field line step count Invert colors configStandalone Heavy OpenGL display display_heavy Show tracks with ITS contribution Show tracks with TOF contribution Enable propagation of loopers Show clusters at nominal x of pad row for early transformed data Split CE tracks when they cross the central Mark clusters Mark adjacent clusters Hide rejected tracks Apply filter on tracks to be displayed Separate increase X h configStandalone Camera window renderer OpenGL display display_camera Orient the camera such that the y axis is always upwards Full Screen MultiSample Anti Downsampling Anti Enable Vertical Sync Target frame or if the backend cannot limit to the refresh Use OpenGL indirect draws to reduce number of draw calls Display h configStandalone Vulkan display display_vulkan Have uniform buffers in host accessible device memory h configStandalone Internal display display_internal Number of updates the Qt GUI may send ahead of the last one the display has before it waits for the display Opacity of the console background Number of console lines the history is shortened to this when it reaches twice the size Time in seconds an info message fades out after setInfoTime h OpenGL display display Font size Do not use Freetype for font Renderer for event Frontend to use for event ROOT macros used as track filter Console script file
Timeframe settings Average distance in cm of events merged into Randomize distance around average distance of merged events Simulate correct bunch interactions instead of placing only the average number of events A value[n] sets TFlen for[n] collisions in Number of bunches per trainsort Number of bunch trains Instantaneous interaction rate Do not simulate border Total number of collisions to be placed in the interior of all time Do not select random but walk over array of events in stride steps h Event generator settings h configStandalone Use CPU for GPU Force usage of the specified GPU device no CPU fallback Continue processing after an error s
Timeframe settings Average distance in cm of events merged into Randomize distance around average distance of merged events Simulate correct bunch interactions instead of placing only the average number of events A value[n] sets TFlen for[n] collisions in Number of bunches per trainsort Number of bunch trains Instantaneous interaction rate Do not simulate border Total number of collisions to be placed in the interior of all time Do not select random but walk over array of events in stride steps h Event generator settings h configStandalone Use CPU for GPU Force usage of the specified GPU device no CPU fallback Continue processing after an error First event to Number of events to Number of iterations to Number of iterations to Number of initial iterations excluded from const char Directory with events to Events directory is and not inside events d
uint32_t side
Definition RawData.h:0
double radius
GLdouble n
Definition glcorearb.h:1982
GLuint GLfloat GLfloat GLfloat GLfloat GLfloat GLfloat GLfloat GLfloat s1
Definition glcorearb.h:5034
GLuint index
Definition glcorearb.h:781
GLdouble GLdouble GLdouble GLdouble top
Definition glcorearb.h:4077
GLint first
Definition glcorearb.h:399
GLenum GLuint GLint GLenum face
Definition glcorearb.h:3184
GLboolean GLboolean GLboolean b
Definition glcorearb.h:1233
GLsizei GLsizei GLfloat distance
Definition glcorearb.h:5506
GLint reference
Definition glcorearb.h:5487
GLsizei const GLfloat * value
Definition glcorearb.h:819
GLenum GLsizei GLsizei GLint * values
Definition glcorearb.h:1576
GLintptr offset
Definition glcorearb.h:660
GLint GLint bottom
Definition glcorearb.h:1979
GLsizei const GLchar *const * path
Definition glcorearb.h:3591
GLboolean r
Definition glcorearb.h:1233
GLboolean GLboolean GLboolean GLboolean a
Definition glcorearb.h:1233
GLsizei const GLint * box
Definition glcorearb.h:4697
bool LoadFlatCSG(const std::string &file, O2FlatCSG &solid)
Load a flat-CSG sidecar (flatcsg_*.bin, version 1) into solid; call CloseShape() after....
bool WriteFlatCSG(const std::string &file, const O2FlatCSG &solid)
std::map< std::string, ID > expected
BOOST_AUTO_TEST_CASE(box_from_six_planes_contains_like_TGeoBBox)
BOOST_CHECK(tree)
BOOST_CHECK_EQUAL(triggersD.size(), triggers.size())
std::vector< int > row