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
int deep
GLint GLsizei count
Definition glcorearb.h:399
GLuint index
Definition glcorearb.h:781
GLuint const GLchar * name
Definition glcorearb.h:781
GLint y
Definition glcorearb.h:270
GLenum target
Definition glcorearb.h:1641
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())