Project
Loading...
Searching...
No Matches
testBVHAssembly.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 O2BVHAssembly class
15#define BOOST_TEST_MAIN
16#define BOOST_TEST_DYN_LINK
17#include <boost/test/unit_test.hpp>
18
20
21#include "TFile.h"
22#include "TGeoBBox.h"
23#include "TGeoManager.h"
24#include "TGeoMaterial.h"
25#include "TGeoMatrix.h"
26#include "TGeoMedium.h"
27#include "TGeoNode.h"
28#include "TGeoShapeAssembly.h"
29#include "TGeoVolume.h"
30
31#include <cmath>
32#include <cstdio>
33#include <filesystem>
34#include <string>
35#include <vector>
36
37namespace
38{
40
42class Rng
43{
44 public:
45 explicit Rng(unsigned long long seed) : mState(seed) {}
46 double uniform(double low, double high)
47 {
48 mState = mState * 6364136223846793005ULL + 1442695040888963407ULL;
49 const double unit = static_cast<double>((mState >> 11) & ((1ULL << 53) - 1)) / static_cast<double>(1ULL << 53);
50 return low + unit * (high - low);
51 }
52 void direction(double* dir)
53 {
54 double norm = 0.;
55 do {
56 for (int index = 0; index < 3; ++index) {
57 dir[index] = uniform(-1., 1.);
58 }
59 norm = std::sqrt(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]);
60 } while (norm < 1.e-3);
61 for (int index = 0; index < 3; ++index) {
62 dir[index] /= norm;
63 }
64 }
65
66 private:
67 unsigned long long mState;
68};
69
70TGeoMedium* vacuum()
71{
72 auto* material = new TGeoMaterial("Vacuum", 0., 0., 0.);
73 return new TGeoMedium("Vacuum", 1, material);
74}
75
78struct Grid {
79 TGeoManager* manager = nullptr;
80 TGeoVolume* world = nullptr;
81 TGeoVolumeAssembly* assembly = nullptr;
82 int count = 0;
83 double pitch = 0.;
84 double halfBox = 0.;
85};
86
87Grid makeGrid(const char* name, int count, double pitch, double halfBox)
88{
89 Grid grid;
90 grid.manager = new TGeoManager(name, name);
91 grid.count = count;
92 grid.pitch = pitch;
93 grid.halfBox = halfBox;
94 auto* medium = vacuum();
95 const double extent = 4. * count * pitch;
96 grid.world = grid.manager->MakeBox("WORLD", medium, extent, extent, extent);
97 grid.assembly = new TGeoVolumeAssembly("GRID");
98 int copy = 0;
99 for (int ix = 0; ix < count; ++ix) {
100 for (int iy = 0; iy < count; ++iy) {
101 for (int iz = 0; iz < count; ++iz) {
102 auto* box = grid.manager->MakeBox(Form("cell_%d", copy), medium, halfBox, halfBox, halfBox);
103 grid.assembly->AddNode(box, copy,
104 new TGeoTranslation(pitch * (ix - 0.5 * (count - 1)),
105 pitch * (iy - 0.5 * (count - 1)),
106 pitch * (iz - 0.5 * (count - 1))));
107 ++copy;
108 }
109 }
110 }
111 grid.world->AddNode(grid.assembly, 1, new TGeoTranslation(0., 0., 0.));
112 grid.manager->SetTopVolume(grid.world);
113 return grid;
114}
115
117double gridReach(const Grid& grid)
118{
119 return 0.5 * grid.pitch * (grid.count - 1) + grid.halfBox;
120}
121
123void clearNodeIndices(TGeoVolumeAssembly* volume)
124{
125 volume->SetCurrentNodeIndex(-1);
126 volume->SetNextNodeIndex(-1);
127}
128} // namespace
129
130// ---------------------------------------------------------------------------------------------
131// Construction
132// ---------------------------------------------------------------------------------------------
133
134BOOST_AUTO_TEST_CASE(BuildsOnePrimitivePerDaughter)
135{
136 Grid grid = makeGrid("build_grid", 5, 3., 1.);
137 auto* shape = new O2BVHAssembly(grid.assembly);
138 BOOST_CHECK_EQUAL(shape->GetNbuilt(), 125);
139 BOOST_CHECK_EQUAL(shape->GetNbuilt(), grid.assembly->GetNdaughters());
140 BOOST_CHECK_GT(shape->GetBVHMemory(), 0u);
141}
142
143BOOST_AUTO_TEST_CASE(BoundingBoxMatchesRoot)
144{
145 Grid grid = makeGrid("bbox_grid", 4, 3., 1.);
146 auto* rootShape = static_cast<TGeoShapeAssembly*>(grid.assembly->GetShape());
147 rootShape->ComputeBBox();
148 auto* shape = new O2BVHAssembly(grid.assembly);
149 const auto* ours = static_cast<const TGeoBBox*>(shape);
150 const auto* theirs = static_cast<const TGeoBBox*>(rootShape);
151 BOOST_CHECK_EQUAL(ours->GetDX(), theirs->GetDX());
152 BOOST_CHECK_EQUAL(ours->GetDY(), theirs->GetDY());
153 BOOST_CHECK_EQUAL(ours->GetDZ(), theirs->GetDZ());
154 for (int axis = 0; axis < 3; ++axis) {
155 BOOST_CHECK_EQUAL(ours->GetOrigin()[axis], theirs->GetOrigin()[axis]);
156 }
157}
158
159BOOST_AUTO_TEST_CASE(EmptyAssemblyAnswersNothing)
160{
161 auto* manager = new TGeoManager("empty_asm", "empty_asm");
162 auto* medium = vacuum();
163 auto* world = manager->MakeBox("WORLD", medium, 10., 10., 10.);
164 auto* assembly = new TGeoVolumeAssembly("EMPTY");
165 world->AddNode(assembly, 1, new TGeoTranslation(0., 0., 0.));
166 manager->SetTopVolume(world);
167 auto* shape = new O2BVHAssembly(assembly);
168 BOOST_CHECK_EQUAL(shape->GetNbuilt(), 0);
169 const double point[3] = {0., 0., 0.};
170 const double direction[3] = {1., 0., 0.};
171 BOOST_CHECK(!shape->Contains(point));
172 BOOST_CHECK_EQUAL(shape->DistFromOutside(point, direction, 3, TGeoShape::Big()), TGeoShape::Big());
173 BOOST_CHECK_EQUAL(shape->Safety(point, kFALSE), TGeoShape::Big());
174}
175
176// ---------------------------------------------------------------------------------------------
177// BVH == Loop, bit for bit
178// ---------------------------------------------------------------------------------------------
179
180BOOST_AUTO_TEST_CASE(ContainsMatchesLoopOnAGrid)
181{
182 Grid grid = makeGrid("contains_grid", 6, 3., 1.);
183 auto* shape = new O2BVHAssembly(grid.assembly);
184 const double reach = 1.3 * gridReach(grid);
185 Rng rng(20260823);
186 int inside = 0;
187 for (int trial = 0; trial < 20000; ++trial) {
188 const double point[3] = {rng.uniform(-reach, reach), rng.uniform(-reach, reach), rng.uniform(-reach, reach)};
189 clearNodeIndices(grid.assembly);
190 const bool fromBVH = shape->Contains(point);
191 const int bvhNode = grid.assembly->GetCurrentNodeIndex();
192 clearNodeIndices(grid.assembly);
193 const bool fromLoop = shape->Contains_Loop(point);
194 const int loopNode = grid.assembly->GetCurrentNodeIndex();
195 BOOST_REQUIRE_EQUAL(fromBVH, fromLoop);
196 BOOST_REQUIRE_EQUAL(bvhNode, loopNode);
197 inside += fromBVH ? 1 : 0;
198 }
199 // the corpus has to actually exercise both verdicts
200 BOOST_CHECK_GT(inside, 100);
201 BOOST_CHECK_LT(inside, 19900);
202}
203
204BOOST_AUTO_TEST_CASE(DistFromOutsideMatchesLoopOnAGrid)
205{
206 Grid grid = makeGrid("dist_grid", 6, 3., 1.);
207 auto* shape = new O2BVHAssembly(grid.assembly);
208 const double start = 3. * gridReach(grid);
209 Rng rng(777);
210 int hits = 0;
211 for (int trial = 0; trial < 5000; ++trial) {
212 double direction[3];
213 rng.direction(direction);
214 const double origin[3] = {-start * direction[0], -start * direction[1], -start * direction[2]};
215 // aim back through a random point in the grid volume
216 const double target[3] = {rng.uniform(-gridReach(grid), gridReach(grid)),
217 rng.uniform(-gridReach(grid), gridReach(grid)),
218 rng.uniform(-gridReach(grid), gridReach(grid))};
219 double aim[3] = {target[0] - origin[0], target[1] - origin[1], target[2] - origin[2]};
220 const double norm = std::sqrt(aim[0] * aim[0] + aim[1] * aim[1] + aim[2] * aim[2]);
221 for (int axis = 0; axis < 3; ++axis) {
222 aim[axis] /= norm;
223 }
224 clearNodeIndices(grid.assembly);
225 const double fromBVH = shape->DistFromOutside(origin, aim, 3, TGeoShape::Big());
226 const int bvhNode = grid.assembly->GetNextNodeIndex();
227 clearNodeIndices(grid.assembly);
228 const double fromLoop = shape->DistFromOutside_Loop(origin, aim, TGeoShape::Big());
229 const int loopNode = grid.assembly->GetNextNodeIndex();
230 BOOST_REQUIRE_EQUAL(fromBVH, fromLoop); // exact: both minimise the same per-daughter numbers
231 BOOST_REQUIRE_EQUAL(bvhNode, loopNode);
232 hits += fromBVH < TGeoShape::Big() ? 1 : 0;
233 }
234 BOOST_CHECK_GT(hits, 1000);
235}
236
237BOOST_AUTO_TEST_CASE(DistFromOutsideRespectsTheStepBound)
238{
239 Grid grid = makeGrid("step_grid", 5, 3., 1.); // odd count, so a cell sits on the axis
240 auto* shape = new O2BVHAssembly(grid.assembly);
241 const double origin[3] = {-50., 0., 0.};
242 const double direction[3] = {1., 0., 0.};
243 const double unbounded = shape->DistFromOutside(origin, direction, 3, TGeoShape::Big());
244 BOOST_REQUIRE_LT(unbounded, TGeoShape::Big());
245 // a bound just short of the crossing must hide it, one just past must not
246 BOOST_CHECK_EQUAL(shape->DistFromOutside(origin, direction, 3, unbounded * 0.5), TGeoShape::Big());
247 BOOST_CHECK_EQUAL(shape->DistFromOutside(origin, direction, 3, unbounded * 1.5), unbounded);
248 BOOST_CHECK_EQUAL(shape->DistFromOutside_Loop(origin, direction, unbounded * 0.5), TGeoShape::Big());
249}
250
251BOOST_AUTO_TEST_CASE(SafetyMatchesLoopOnAGrid)
252{
253 Grid grid = makeGrid("safety_grid", 6, 3., 1.);
254 auto* shape = new O2BVHAssembly(grid.assembly);
255 const double reach = 1.5 * gridReach(grid);
256 Rng rng(4242);
257 int positive = 0;
258 for (int trial = 0; trial < 4000; ++trial) {
259 const double point[3] = {rng.uniform(-reach, reach), rng.uniform(-reach, reach), rng.uniform(-reach, reach)};
260 const double fromBVH = shape->Safety(point, kFALSE);
261 const double fromLoop = shape->Safety_Loop(point, kFALSE);
262 BOOST_REQUIRE_EQUAL(fromBVH, fromLoop); // exact: the traversal prunes only on a lower bound
263 positive += fromBVH > 0. ? 1 : 0;
264 }
265 BOOST_CHECK_GT(positive, 100);
266}
267
268// ---------------------------------------------------------------------------------------------
269// Agreement with ROOT
270// ---------------------------------------------------------------------------------------------
271
272BOOST_AUTO_TEST_CASE(ContainsAgreesWithRootOnAClosedGeometry)
273{
274 Grid grid = makeGrid("root_contains", 6, 3., 1.);
275 grid.manager->CloseGeometry();
276 BOOST_REQUIRE(grid.assembly->GetVoxels() != nullptr); // ROOT's accelerated path, not the linear one
277 auto* rootShape = static_cast<TGeoShapeAssembly*>(grid.assembly->GetShape());
278 auto* shape = new O2BVHAssembly(grid.assembly);
279 const double reach = 1.3 * gridReach(grid);
280 Rng rng(31337);
281 for (int trial = 0; trial < 10000; ++trial) {
282 const double point[3] = {rng.uniform(-reach, reach), rng.uniform(-reach, reach), rng.uniform(-reach, reach)};
283 const bool fromRoot = rootShape->Contains(point);
284 const int rootNode = grid.assembly->GetCurrentNodeIndex();
285 const bool fromBVH = shape->Contains(point);
286 BOOST_REQUIRE_EQUAL(fromRoot, fromBVH);
287 if (fromBVH) {
288 BOOST_REQUIRE_EQUAL(rootNode, grid.assembly->GetCurrentNodeIndex()); // the same daughter, not just a daughter
289 }
290 }
291}
292
298BOOST_AUTO_TEST_CASE(SafetyIsNeverLargerThanRoot)
299{
300 Grid grid = makeGrid("root_safety", 5, 3., 1.);
301 grid.manager->CloseGeometry();
302 auto* rootShape = static_cast<TGeoShapeAssembly*>(grid.assembly->GetShape());
303 auto* shape = new O2BVHAssembly(grid.assembly);
304 const double reach = 1.5 * gridReach(grid);
305 Rng rng(99);
306 int rootTooLarge = 0;
307 for (int trial = 0; trial < 2000; ++trial) {
308 const double point[3] = {rng.uniform(-reach, reach), rng.uniform(-reach, reach), rng.uniform(-reach, reach)};
309 const double ours = shape->Safety(point, kFALSE);
310 const double theirs = rootShape->Safety(point, kFALSE);
311 BOOST_REQUIRE_EQUAL(ours, shape->Safety_Loop(point, kFALSE));
312 BOOST_REQUIRE_LE(ours, theirs);
313 rootTooLarge += theirs > ours ? 1 : 0;
314 }
315 BOOST_TEST_MESSAGE("ROOT returned more than the daughter minimum on " << rootTooLarge << " of 2000 points");
316}
317
322BOOST_AUTO_TEST_CASE(DistFromOutsideIsNeverWorseThanRoot)
323{
324 Grid grid = makeGrid("root_dist", 6, 3., 1.);
325 grid.manager->CloseGeometry();
326 BOOST_REQUIRE(grid.assembly->GetVoxels() != nullptr);
327 auto* rootShape = static_cast<TGeoShapeAssembly*>(grid.assembly->GetShape());
328 auto* shape = new O2BVHAssembly(grid.assembly);
329 const double start = 3. * gridReach(grid);
330 Rng rng(2024);
331 int weFound = 0;
332 int rootGaveUp = 0;
333 for (int trial = 0; trial < 2000; ++trial) {
334 double direction[3];
335 rng.direction(direction);
336 const double origin[3] = {-start * direction[0], -start * direction[1], -start * direction[2]};
337 const double target[3] = {rng.uniform(-gridReach(grid), gridReach(grid)),
338 rng.uniform(-gridReach(grid), gridReach(grid)),
339 rng.uniform(-gridReach(grid), gridReach(grid))};
340 double aim[3] = {target[0] - origin[0], target[1] - origin[1], target[2] - origin[2]};
341 const double norm = std::sqrt(aim[0] * aim[0] + aim[1] * aim[1] + aim[2] * aim[2]);
342 for (int axis = 0; axis < 3; ++axis) {
343 aim[axis] /= norm;
344 }
345 const double ours = shape->DistFromOutside(origin, aim, 3, TGeoShape::Big());
346 const double theirs = rootShape->DistFromOutside(origin, aim, 3, TGeoShape::Big());
347 BOOST_REQUIRE_EQUAL(ours, shape->DistFromOutside_Loop(origin, aim, TGeoShape::Big()));
348 if (theirs < TGeoShape::Big()) {
349 BOOST_REQUIRE_EQUAL(theirs, ours);
350 } else {
351 ++rootGaveUp;
352 }
353 weFound += ours < TGeoShape::Big() ? 1 : 0;
354 }
355 BOOST_CHECK_GT(weFound, 500);
356 BOOST_TEST_MESSAGE("ROOT returned Big() on " << rootGaveUp << " of 2000 rays this class answered");
357}
358
359// ---------------------------------------------------------------------------------------------
360// Overlaps, nesting, rotations
361// ---------------------------------------------------------------------------------------------
362
363BOOST_AUTO_TEST_CASE(OverlappingDaughtersResolveToTheLowestIndex)
364{
365 auto* manager = new TGeoManager("overlap_asm", "overlap_asm");
366 auto* medium = vacuum();
367 auto* world = manager->MakeBox("WORLD", medium, 50., 50., 50.);
368 auto* assembly = new TGeoVolumeAssembly("OVERLAP");
369 // five boxes each shifted by half their width: every interior point sits in two of them
370 for (int index = 0; index < 5; ++index) {
371 auto* box = manager->MakeBox(Form("ov_%d", index), medium, 2., 2., 2.);
372 assembly->AddNode(box, index, new TGeoTranslation(2. * index, 0., 0.));
373 }
374 world->AddNode(assembly, 1, new TGeoTranslation(0., 0., 0.));
375 manager->SetTopVolume(world);
376 auto* shape = new O2BVHAssembly(assembly);
377 Rng rng(5);
378 int overlaps = 0;
379 for (int trial = 0; trial < 5000; ++trial) {
380 const double point[3] = {rng.uniform(-4., 12.), rng.uniform(-3., 3.), rng.uniform(-3., 3.)};
381 clearNodeIndices(assembly);
382 const bool fromBVH = shape->Contains(point);
383 const int bvhNode = assembly->GetCurrentNodeIndex();
384 clearNodeIndices(assembly);
385 const bool fromLoop = shape->Contains_Loop(point);
386 const int loopNode = assembly->GetCurrentNodeIndex();
387 BOOST_REQUIRE_EQUAL(fromBVH, fromLoop);
388 BOOST_REQUIRE_EQUAL(bvhNode, loopNode);
389 if (fromBVH) {
390 int count = 0;
391 double local[3];
392 for (int index = 0; index < assembly->GetNdaughters(); ++index) {
393 assembly->GetNode(index)->MasterToLocal(point, local);
394 count += assembly->GetNode(index)->GetVolume()->GetShape()->Contains(local) ? 1 : 0;
395 }
396 overlaps += count > 1 ? 1 : 0;
397 }
398 }
399 BOOST_CHECK_GT(overlaps, 100); // the corpus really does have shared points
400}
401
402BOOST_AUTO_TEST_CASE(NestedAssembliesAgreeWithTheLoop)
403{
404 auto* manager = new TGeoManager("nested_asm", "nested_asm");
405 auto* medium = vacuum();
406 auto* world = manager->MakeBox("WORLD", medium, 100., 100., 100.);
407 auto* outer = new TGeoVolumeAssembly("OUTER");
408 for (int block = 0; block < 6; ++block) {
409 auto* inner = new TGeoVolumeAssembly(Form("INNER_%d", block));
410 for (int cell = 0; cell < 8; ++cell) {
411 auto* box = manager->MakeBox(Form("n_%d_%d", block, cell), medium, 1., 1., 1.);
412 inner->AddNode(box, cell, new TGeoTranslation(2.5 * cell, 0., 0.));
413 }
414 auto* rotation = new TGeoRotation(Form("rot_%d", block), 13. * block, 7. * block, 5. * block);
415 outer->AddNode(inner, block, new TGeoCombiTrans(0., 6. * block, 0., rotation));
416 }
417 world->AddNode(outer, 1, new TGeoTranslation(0., 0., 0.));
418 manager->SetTopVolume(world);
419 auto* shape = new O2BVHAssembly(outer);
420 BOOST_CHECK_EQUAL(shape->GetNbuilt(), 6);
421 Rng rng(606);
422 int inside = 0;
423 int hits = 0;
424 for (int trial = 0; trial < 4000; ++trial) {
425 const double point[3] = {rng.uniform(-5., 25.), rng.uniform(-5., 35.), rng.uniform(-5., 5.)};
426 clearNodeIndices(outer);
427 const bool fromBVH = shape->Contains(point);
428 const int bvhNode = outer->GetCurrentNodeIndex();
429 clearNodeIndices(outer);
430 const bool fromLoop = shape->Contains_Loop(point);
431 BOOST_REQUIRE_EQUAL(fromBVH, fromLoop);
432 BOOST_REQUIRE_EQUAL(bvhNode, outer->GetCurrentNodeIndex());
433 inside += fromBVH ? 1 : 0;
434
435 double direction[3];
436 rng.direction(direction);
437 const double origin[3] = {point[0] - 200. * direction[0], point[1] - 200. * direction[1],
438 point[2] - 200. * direction[2]};
439 const double distanceBVH = shape->DistFromOutside(origin, direction, 3, TGeoShape::Big());
440 const double distanceLoop = shape->DistFromOutside_Loop(origin, direction, TGeoShape::Big());
441 BOOST_REQUIRE_EQUAL(distanceBVH, distanceLoop);
442 hits += distanceBVH < TGeoShape::Big() ? 1 : 0;
443
444 BOOST_REQUIRE_EQUAL(shape->Safety(point, kFALSE), shape->Safety_Loop(point, kFALSE));
445 }
446 BOOST_CHECK_GT(inside, 50);
447 BOOST_CHECK_GT(hits, 500);
448}
449
450BOOST_AUTO_TEST_CASE(RotatedDaughtersAgreeWithTheLoop)
451{
452 auto* manager = new TGeoManager("rotated_asm", "rotated_asm");
453 auto* medium = vacuum();
454 auto* world = manager->MakeBox("WORLD", medium, 100., 100., 100.);
455 auto* assembly = new TGeoVolumeAssembly("ROTATED");
456 for (int index = 0; index < 40; ++index) {
457 auto* box = manager->MakeBox(Form("r_%d", index), medium, 3., 0.5, 2.);
458 auto* rotation = new TGeoRotation(Form("rr_%d", index), 9. * index, 4. * index, 17. * index);
459 assembly->AddNode(box, index,
460 new TGeoCombiTrans(8. * std::cos(0.31 * index), 8. * std::sin(0.31 * index), 0.7 * index - 14.,
461 rotation));
462 }
463 world->AddNode(assembly, 1, new TGeoTranslation(0., 0., 0.));
464 manager->SetTopVolume(world);
465 auto* shape = new O2BVHAssembly(assembly);
466 Rng rng(818);
467 for (int trial = 0; trial < 4000; ++trial) {
468 const double point[3] = {rng.uniform(-15., 15.), rng.uniform(-15., 15.), rng.uniform(-20., 20.)};
469 BOOST_REQUIRE_EQUAL(shape->Contains(point), shape->Contains_Loop(point));
470 BOOST_REQUIRE_EQUAL(shape->Safety(point, kFALSE), shape->Safety_Loop(point, kFALSE));
471 double direction[3];
472 rng.direction(direction);
473 BOOST_REQUIRE_EQUAL(shape->DistFromOutside(point, direction, 3, TGeoShape::Big()),
474 shape->DistFromOutside_Loop(point, direction, TGeoShape::Big()));
475 }
476}
477
478// ---------------------------------------------------------------------------------------------
479// Edge cases the tolerance discipline exists for
480// ---------------------------------------------------------------------------------------------
481
482BOOST_AUTO_TEST_CASE(PointsOnSharedFacesAgreeWithTheLoop)
483{
484 // touching cells: pitch equals the box width, so consecutive cells share a face exactly
485 Grid grid = makeGrid("faces_grid", 5, 2., 1.);
486 auto* shape = new O2BVHAssembly(grid.assembly);
487 const double first = -0.5 * grid.pitch * (grid.count - 1);
488 int checked = 0;
489 for (int ix = 0; ix < grid.count; ++ix) {
490 for (int iy = 0; iy < grid.count; ++iy) {
491 for (int iz = 0; iz < grid.count; ++iz) {
492 // the +x face of cell (ix,iy,iz), which is the -x face of its neighbour
493 const double point[3] = {first + grid.pitch * ix + grid.halfBox, first + grid.pitch * iy,
494 first + grid.pitch * iz};
495 clearNodeIndices(grid.assembly);
496 const bool fromBVH = shape->Contains(point);
497 const int bvhNode = grid.assembly->GetCurrentNodeIndex();
498 clearNodeIndices(grid.assembly);
499 BOOST_REQUIRE_EQUAL(fromBVH, shape->Contains_Loop(point));
500 BOOST_REQUIRE_EQUAL(bvhNode, grid.assembly->GetCurrentNodeIndex());
501 BOOST_REQUIRE_EQUAL(shape->Safety(point, kFALSE), shape->Safety_Loop(point, kFALSE));
502 ++checked;
503 }
504 }
505 }
506 BOOST_CHECK_EQUAL(checked, 125);
507}
508
509BOOST_AUTO_TEST_CASE(RaysAlongASeamAgreeWithTheLoop)
510{
511 Grid grid = makeGrid("seam_grid", 5, 2., 1.);
512 auto* shape = new O2BVHAssembly(grid.assembly);
513 const double first = -0.5 * grid.pitch * (grid.count - 1);
514 const double start = 4. * gridReach(grid);
515 int checked = 0;
516 for (int iy = 0; iy < grid.count; ++iy) {
517 for (int iz = 0; iz < grid.count; ++iz) {
518 for (int offset = -1; offset <= 1; ++offset) {
519 // a ray running exactly along the plane where two rows of cells touch
520 const double y = first + grid.pitch * iy + offset * grid.halfBox;
521 const double origin[3] = {-start, y, first + grid.pitch * iz};
522 const double direction[3] = {1., 0., 0.};
523 clearNodeIndices(grid.assembly);
524 const double fromBVH = shape->DistFromOutside(origin, direction, 3, TGeoShape::Big());
525 const int bvhNode = grid.assembly->GetNextNodeIndex();
526 clearNodeIndices(grid.assembly);
527 BOOST_REQUIRE_EQUAL(fromBVH, shape->DistFromOutside_Loop(origin, direction, TGeoShape::Big()));
528 BOOST_REQUIRE_EQUAL(bvhNode, grid.assembly->GetNextNodeIndex());
529 ++checked;
530 }
531 }
532 }
533 BOOST_CHECK_EQUAL(checked, 75);
534}
535
536BOOST_AUTO_TEST_CASE(RaysAlongTheCoordinateAxesAgreeWithTheLoop)
537{
538 Grid grid = makeGrid("axis_grid", 5, 3., 1.);
539 auto* shape = new O2BVHAssembly(grid.assembly);
540 const double start = 4. * gridReach(grid);
541 const double first = -0.5 * grid.pitch * (grid.count - 1);
542 for (int axis = 0; axis < 3; ++axis) {
543 for (int step = 0; step < grid.count; ++step) {
544 double origin[3] = {0., 0., 0.};
545 double direction[3] = {0., 0., 0.};
546 origin[axis] = -start;
547 direction[axis] = 1.;
548 origin[(axis + 1) % 3] = first + grid.pitch * step;
549 const double fromBVH = shape->DistFromOutside(origin, direction, 3, TGeoShape::Big());
550 BOOST_REQUIRE_EQUAL(fromBVH, shape->DistFromOutside_Loop(origin, direction, TGeoShape::Big()));
551 BOOST_REQUIRE_LT(fromBVH, TGeoShape::Big());
552 }
553 }
554}
555
556// ---------------------------------------------------------------------------------------------
557// Lifecycle: lazy rebuild, shape swap, navigation, I/O
558// ---------------------------------------------------------------------------------------------
559
560BOOST_AUTO_TEST_CASE(AddingADaughterRebuildsLazily)
561{
562 auto* manager = new TGeoManager("lazy_asm", "lazy_asm");
563 auto* medium = vacuum();
564 auto* world = manager->MakeBox("WORLD", medium, 50., 50., 50.);
565 auto* assembly = new TGeoVolumeAssembly("LAZY");
566 auto* firstBox = manager->MakeBox("lz_0", medium, 1., 1., 1.);
567 assembly->AddNode(firstBox, 0, new TGeoTranslation(0., 0., 0.));
568 world->AddNode(assembly, 1, new TGeoTranslation(0., 0., 0.));
569 manager->SetTopVolume(world);
570
571 auto* shape = new O2BVHAssembly(assembly);
572 assembly->SetShape(shape);
573 const double newPoint[3] = {10., 0., 0.};
574 BOOST_CHECK(!shape->Contains(newPoint));
575
576 auto* secondBox = manager->MakeBox("lz_1", medium, 1., 1., 1.);
577 assembly->AddNode(secondBox, 1, new TGeoTranslation(10., 0., 0.));
578 // AddNode invalidated the base bounding box; the BVH notices the new daughter count by itself
579 BOOST_CHECK(shape->Contains(newPoint));
580 BOOST_CHECK_EQUAL(assembly->GetCurrentNodeIndex(), 1);
581 BOOST_CHECK_EQUAL(shape->GetNbuilt(), 2);
582}
583
584BOOST_AUTO_TEST_CASE(MakeBVHAssemblySwapsTheShapeAndKeepsTheVoxels)
585{
586 Grid grid = makeGrid("swap_asm", 5, 3., 1.);
587 grid.manager->CloseGeometry();
588 BOOST_REQUIRE(grid.assembly->GetVoxels() != nullptr);
589 auto* shape = O2BVHAssembly::MakeBVHAssembly(grid.assembly);
590 BOOST_REQUIRE(shape != nullptr);
591 BOOST_CHECK_EQUAL(grid.assembly->GetShape(), static_cast<TGeoShape*>(shape));
592 BOOST_CHECK(grid.assembly->IsAssembly());
593 BOOST_CHECK(shape->IsAssembly());
594 BOOST_CHECK_EQUAL(shape->GetNbuilt(), 125);
595 // the finder stays by default: TGeoNavigator::SearchNode reads it once it is inside the
596 // assembly, and dropping it turns point location into a linear walk
597 BOOST_CHECK(grid.assembly->GetVoxels() != nullptr);
598 BOOST_CHECK(O2BVHAssembly::MakeBVHAssembly(nullptr) == nullptr);
599}
600
601BOOST_AUTO_TEST_CASE(NavigationFindsTheSameLeafBeforeAndAfterTheSwap)
602{
603 Grid grid = makeGrid("nav_asm", 5, 3., 1.);
604 grid.manager->CloseGeometry();
605 const double reach = 1.2 * gridReach(grid);
606 Rng rng(1234);
607 std::vector<double> points;
608 std::vector<std::string> paths;
609 for (int trial = 0; trial < 3000; ++trial) {
610 const double point[3] = {rng.uniform(-reach, reach), rng.uniform(-reach, reach), rng.uniform(-reach, reach)};
611 grid.manager->FindNode(point[0], point[1], point[2]);
612 points.insert(points.end(), {point[0], point[1], point[2]});
613 paths.emplace_back(grid.manager->GetPath());
614 }
615 O2BVHAssembly::MakeBVHAssembly(grid.assembly);
616 int deep = 0;
617 for (size_t trial = 0; trial < paths.size(); ++trial) {
618 grid.manager->FindNode(points[3 * trial], points[3 * trial + 1], points[3 * trial + 2]);
619 BOOST_REQUIRE_EQUAL(paths[trial], std::string(grid.manager->GetPath()));
620 deep += paths[trial].find("/cell_") != std::string::npos ? 1 : 0;
621 }
622 BOOST_CHECK_GT(deep, 100); // the corpus really does reach the leaves through the assembly
623}
624
625BOOST_AUTO_TEST_CASE(TransportCrossesTheSameLeavesAsRoot)
626{
627 Grid reference = makeGrid("transport_root", 5, 3., 1.);
628 reference.manager->CloseGeometry();
629 const double start = 3. * gridReach(reference);
630 Rng rng(24680);
631 std::vector<double> origins;
632 std::vector<double> directions;
633 std::vector<std::vector<std::string>> rootPaths;
634 for (int ray = 0; ray < 200; ++ray) {
635 double direction[3];
636 rng.direction(direction);
637 const double origin[3] = {-start * direction[0], -start * direction[1], -start * direction[2]};
638 origins.insert(origins.end(), {origin[0], origin[1], origin[2]});
639 directions.insert(directions.end(), {direction[0], direction[1], direction[2]});
640 reference.manager->InitTrack(origin, direction);
641 std::vector<std::string> path;
642 int guard = 0;
643 while (!reference.manager->IsOutside() && guard++ < 500) {
644 reference.manager->FindNextBoundaryAndStep(1.e10);
645 path.emplace_back(reference.manager->GetPath());
646 }
647 rootPaths.push_back(path);
648 }
649
650 O2BVHAssembly::MakeBVHAssembly(reference.assembly);
651 int crossings = 0;
652 for (int ray = 0; ray < 200; ++ray) {
653 reference.manager->InitTrack(&origins[3 * ray], &directions[3 * ray]);
654 std::vector<std::string> path;
655 int guard = 0;
656 while (!reference.manager->IsOutside() && guard++ < 500) {
657 reference.manager->FindNextBoundaryAndStep(1.e10);
658 path.emplace_back(reference.manager->GetPath());
659 }
660 // this class only ever finds *more* than ROOT (section 4 of the stream document), so the
661 // requirement is that everything ROOT saw is still seen, in order
662 BOOST_REQUIRE_GE(path.size(), rootPaths[ray].size());
663 for (const auto& step : rootPaths[ray]) {
664 crossings += step.find("/cell_") != std::string::npos ? 1 : 0;
665 }
666 if (path.size() == rootPaths[ray].size()) {
667 BOOST_REQUIRE(path == rootPaths[ray]);
668 }
669 }
670 BOOST_CHECK_GT(crossings, 100);
671}
672
673BOOST_AUTO_TEST_CASE(SurvivesAGeometryRoundTrip)
674{
675 const std::string file = "testBVHAssembly_roundtrip.root";
676 Grid grid = makeGrid("io_asm", 4, 3., 1.);
677 grid.manager->CloseGeometry();
678 O2BVHAssembly::MakeBVHAssembly(grid.assembly);
679 const double reach = 1.2 * gridReach(grid);
680 Rng rng(1111);
681 std::vector<double> points;
682 std::vector<int> nodes;
683 auto* shape = static_cast<O2BVHAssembly*>(grid.assembly->GetShape());
684 for (int trial = 0; trial < 2000; ++trial) {
685 const double point[3] = {rng.uniform(-reach, reach), rng.uniform(-reach, reach), rng.uniform(-reach, reach)};
686 points.insert(points.end(), {point[0], point[1], point[2]});
687 clearNodeIndices(grid.assembly);
688 shape->Contains(point);
689 nodes.push_back(grid.assembly->GetCurrentNodeIndex());
690 }
691 grid.manager->Export(file.c_str());
692
693 auto* reloaded = TGeoManager::Import(file.c_str());
694 BOOST_REQUIRE(reloaded != nullptr);
695 auto* reloadedAssembly = dynamic_cast<TGeoVolumeAssembly*>(reloaded->GetTopVolume()->GetNode(0)->GetVolume());
696 BOOST_REQUIRE(reloadedAssembly != nullptr);
697 auto* reloadedShape = dynamic_cast<O2BVHAssembly*>(reloadedAssembly->GetShape());
698 BOOST_REQUIRE(reloadedShape != nullptr); // the shape survived streaming as itself
699 for (size_t trial = 0; trial < nodes.size(); ++trial) {
700 clearNodeIndices(reloadedAssembly);
701 reloadedShape->Contains(&points[3 * trial]);
702 BOOST_REQUIRE_EQUAL(nodes[trial], reloadedAssembly->GetCurrentNodeIndex());
703 }
704 BOOST_CHECK_EQUAL(reloadedShape->GetNbuilt(), 64); // rebuilt lazily on the first query
705 std::error_code ignored;
706 std::filesystem::remove(file, ignored);
707}
header::DataOrigin origin
std::unique_ptr< expressions::Node > rootNode
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
int deep
Acts::RotationMatrix3 rotation
GLint GLsizei count
Definition glcorearb.h:399
GLuint index
Definition glcorearb.h:781
GLuint const GLchar * name
Definition glcorearb.h:781
GLint first
Definition glcorearb.h:399
GLint y
Definition glcorearb.h:270
GLint reference
Definition glcorearb.h:5487
GLsizei const GLuint * paths
Definition glcorearb.h:5475
GLintptr offset
Definition glcorearb.h:660
GLsizei const GLchar *const * path
Definition glcorearb.h:3591
GLuint start
Definition glcorearb.h:469
GLsizei const GLint * box
Definition glcorearb.h:4697
BOOST_AUTO_TEST_CASE(BuildsOnePrimitivePerDaughter)
BOOST_CHECK(tree)
BOOST_CHECK_EQUAL(triggersD.size(), triggers.size())