Skip to content

Commit 9fafcfa

Browse files
sawenzelclaude
andcommitted
Add O2FlatCSG
This adds O2FlatCSG, a flat CSG solid that represents a CAD part as cells of halfspaces. - O2FlatCSG stores its halfspaces and cells in flat POD arrays and accelerates queries with a BVH over sub-cell boxes. - O2SurfaceSolidIO reads and writes the flatcsg_*.bin sidecar. - A #pragma read rule closes every O2FlatCSG read from a file, so a reader gets the accelerated shape. - O2SolidHarness reports a shape whose sub-cell boxes could not be rebuilt. - O2OverlapCheck requires a boundary point of an O2FlatCSG to flip containment. - testFlatCSG covers the solid and o2-bench-cadsupport-xray times transport through it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 685d72f commit 9fafcfa

10 files changed

Lines changed: 5601 additions & 2 deletions

File tree

Detectors/CADSupport/CMakeLists.txt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@ o2_add_library(CADSupport
1717
src/O2OverlapCheck.cxx
1818
src/O2SolidHarness.cxx
1919
src/CADGeometryUtils.cxx
20+
src/O2FlatCSG.cxx
2021
PRIVATE_INCLUDE_DIRECTORIES ${CMAKE_SOURCE_DIR}/Detectors/Base/src
2122
PUBLIC_LINK_LIBRARIES O2::DetectorsBase ROOT::Geom)
2223

2324
o2_target_root_dictionary(CADSupport
2425
HEADERS include/CADSupport/O2BVHSurfaceSolid.h
2526
include/CADSupport/O2BVHAssembly.h
27+
include/CADSupport/O2FlatCSG.h
2628
LINKDEF src/CADSupportLinkDef.h)
2729

2830
o2_add_test(
@@ -39,6 +41,13 @@ o2_add_test(
3941
PUBLIC_LINK_LIBRARIES O2::CADSupport ROOT::Geom ROOT::RIO
4042
LABELS cadsupport)
4143

44+
o2_add_test(
45+
FlatCSG
46+
SOURCES test/testFlatCSG.cxx
47+
COMPONENT_NAME CADSupport
48+
PUBLIC_LINK_LIBRARIES O2::CADSupport ROOT::Geom ROOT::RIO
49+
LABELS cadsupport)
50+
4251
o2_add_executable(
4352
solid-harness
4453
COMPONENT_NAME CADSupport
@@ -47,6 +56,15 @@ o2_add_executable(
4756
PUBLIC_LINK_LIBRARIES O2::CADSupport ROOT::Geom ROOT::RIO
4857
nlohmann_json::nlohmann_json)
4958

59+
# X-ray / geantino transport benchmark.
60+
o2_add_executable(
61+
xray
62+
COMPONENT_NAME CADSupport
63+
SOURCES test/runXRayBenchmark.cxx
64+
IS_BENCHMARK
65+
PUBLIC_LINK_LIBRARIES O2::CADSupport ROOT::Geom ROOT::RIO
66+
nlohmann_json::nlohmann_json)
67+
5068
# Overlap census of a placed geometry.
5169
o2_add_executable(
5270
overlap
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
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.
11+
/// \author Sandro Wenzel <sandro.wenzel@cern.ch>
12+
/// \since 2026-08
13+
14+
#ifndef ALICEO2_CADSUPPORT_O2FLATCSG_
15+
#define ALICEO2_CADSUPPORT_O2FLATCSG_
16+
17+
#include "TGeoBBox.h"
18+
19+
#include <vector>
20+
21+
namespace o2
22+
{
23+
namespace cad
24+
{
25+
26+
/// One signed implicit halfspace, the region `sign * f(x) <= 0`: kQuadric stores `x^T A x + 2 b^T x + c` as
27+
/// (a00, a01, a02, a11, a12, a22, b0, b1, b2, c); kTorus stores (px, py, pz, dx, dy, dz, R, r) in the first eight.
28+
struct FlatCSGHalfspace {
29+
enum Kind : int { kQuadric = 0,
30+
kTorus = 1 };
31+
int kind = kQuadric;
32+
double sign = 1.;
33+
double c[11] = {};
34+
};
35+
36+
/// One DNF cell: `[first, first + count)` of the halfspace array, intersected; `volume` is its own volume.
37+
struct FlatCSGCell {
38+
int first = 0;
39+
int count = 0;
40+
double volume = 0.;
41+
};
42+
43+
/// One box of the sub-cell subdivision; `nActive == 0` means it is wholly inside its cell.
44+
/// An active list describes its cell only inside its box, so every ray query clips to the box first.
45+
struct FlatCSGBox {
46+
double min[3] = {};
47+
double max[3] = {};
48+
int cell = -1;
49+
int firstActive = 0;
50+
int nActive = 0;
51+
};
52+
53+
/// A solid stored as a union of intersection cells over signed implicit halfspaces, the flat DNF of a decomposed part.
54+
/// Every accelerated query has a bit-identical `_Loop` twin over all cells and halfspaces.
55+
class O2FlatCSG : public TGeoBBox
56+
{
57+
public:
58+
O2FlatCSG();
59+
explicit O2FlatCSG(const char* name);
60+
~O2FlatCSG() override;
61+
62+
// The shape owns a raw `bvh::v2::Bvh` behind `fBVH`, so a compiler-written copy would hand two
63+
// shapes the same BVH and then free it twice; same treatment as O2BVHAssembly.
64+
O2FlatCSG(const O2FlatCSG&) = delete;
65+
O2FlatCSG& operator=(const O2FlatCSG&) = delete;
66+
67+
// ---- building -------------------------------------------------------------------------
68+
/// Append a quadric halfspace; returns its index. `sign` is +1 or -1, inside is `sign*Q <= 0`.
69+
int AddQuadric(double sign, const double coeff[10]);
70+
/// Append a torus halfspace, inside `sign * (sqrt((rho - major)^2 + z^2) - minor) <= 0` about unit \a axis; returns its index.
71+
int AddTorus(double sign, const double* centre, const double* axis, double major, double minor);
72+
/// Append a cell over `[first, first + count)` of the halfspace array; returns its index.
73+
int AddCell(int first, int count, double volume);
74+
75+
int GetNhalfspaces() const { return static_cast<int>(fHalfspaces.size()); }
76+
int GetNcells() const { return static_cast<int>(fCells.size()); }
77+
const FlatCSGHalfspace& GetHalfspace(int index) const { return fHalfspaces[index]; }
78+
const FlatCSGCell& GetCell(int index) const { return fCells[index]; }
79+
80+
/// The AABB of cell \a cell. The halfspaces alone do not bound a cell -- an intersection of
81+
/// halfspaces can be unbounded -- so the converter supplies the box the decomposition measured.
82+
void SetCellBBox(int cell, const double* lo, const double* hi);
83+
/// The AABB `SetCellBBox` recorded for cell \a cell, for the sidecar writer. Reads back zeros
84+
/// for a cell whose box was never set.
85+
void GetCellBBox(int cell, double* lo, double* hi) const;
86+
87+
/// Build the sub-cell boxes and their BVH. Call once, after the last AddCell.
88+
void CloseShape();
89+
bool IsClosed() const { return fClosed; }
90+
91+
/// Bytes held by the BVH nodes and the primitive-index permutation.
92+
size_t GetBVHMemory() const;
93+
94+
int GetNboxes() const { return static_cast<int>(fBoxes.size()); }
95+
const FlatCSGBox& GetBox(int index) const { return fBoxes[index]; }
96+
/// For the tests: the box structure is the thing being proved sound, so it has to be readable.
97+
int GetActive(int index) const { return fActive[index]; }
98+
/// True when every halfspace of cell `index` contains `point`.
99+
bool CellContains(int index, const double* point) const;
100+
101+
/// Subdivision depth cap. See `fSplitDepth` for where the default comes from.
102+
void SetSplitDepth(int depth) { fSplitDepth = depth; }
103+
/// Stop splitting a box narrower than this fraction of the part's bounding-box diagonal.
104+
/// See `fMinBoxFraction` for where the default comes from.
105+
void SetMinBoxFraction(double fraction) { fMinBoxFraction = fraction; }
106+
107+
/// `sign * f(point)`; the halfspace contains the point when this is `<= 0`.
108+
static double EvalHalfspace(const FlatCSGHalfspace& halfspace, const double* point);
109+
110+
/// A rigorous enclosure `[rangeLo, rangeHi]` of `sign * f` over the box `[lo, hi]`, padded outward.
111+
/// Requires `lo[i] <= hi[i]` and finite bounds, which CloseShape enforces; it does not check them.
112+
static void HalfspaceRange(const FlatCSGHalfspace& halfspace, const double* lo, const double* hi,
113+
double& rangeLo, double& rangeHi);
114+
115+
/// Real roots of `sign * f(origin + t*dir) = 0`, unsorted, at most four; returns the count.
116+
static int HalfspaceRoots(const FlatCSGHalfspace& halfspace, const double* origin,
117+
const double* dir, double* roots);
118+
119+
/// The occupancy of cell \a cell along the ray within `[tlo, thi]`, as `[enter, exit]` pairs in \a out; a null \a active uses every halfspace.
120+
/// Returns the pair count, or a negative value when \a maxOut is too small.
121+
int CellIntervals(int cell, const int* active, int nActive, const double* origin,
122+
const double* dir, double tlo, double thi, double* out, int maxOut) const;
123+
124+
// ---- the TGeoShape contract; the accelerated queries use their `_Loop` twin until CloseShape succeeds ----
125+
Bool_t Contains(const Double_t* point) const override;
126+
127+
Double_t DistFromOutside(const Double_t* point, const Double_t* dir, Int_t iact = 1,
128+
Double_t step = TGeoShape::Big(), Double_t* safe = nullptr) const override;
129+
Double_t DistFromInside(const Double_t* point, const Double_t* dir, Int_t iact = 1,
130+
Double_t step = TGeoShape::Big(), Double_t* safe = nullptr) const override;
131+
132+
/// A lower bound on the distance to the boundary from the box structure: outside the nearest box, inside the faces of a solid box, else 0.
133+
Double_t Safety(const Double_t* point, Bool_t in = kTRUE) const override;
134+
135+
/// Per-thread count of DistFromInside queries whose pruned traversal had to be redone unpruned.
136+
static void ResetUnprunedRetryCounter();
137+
static long long GetUnprunedRetryCount();
138+
139+
/// The union of the retained sub-cell boxes, tighter than the union of the cell AABBs.
140+
void ComputeBBox() override;
141+
142+
/// The sum of the cells' own volumes. The cells of a decomposition are disjoint by construction
143+
/// (`decompose`'s volume guard checks it), so there is no inclusion-exclusion to do.
144+
Double_t Capacity() const override;
145+
146+
/// The normal of the halfspace nearest to equality at `point`, oriented along `dir`.
147+
void ComputeNormal(const Double_t* point, const Double_t* dir, Double_t* norm) const override;
148+
/// Points on the solid's own boundary, for the overlap checkers; kFALSE if fewer than \a npoints were found.
149+
Bool_t GetPointsOnSegments(Int_t npoints, Double_t* array) const override;
150+
151+
// ---- the reference twins ---------------------------------------------------------------
152+
Bool_t Contains_Loop(const Double_t* point) const;
153+
154+
Double_t DistFromOutside_Loop(const Double_t* point, const Double_t* dir,
155+
Double_t step = TGeoShape::Big()) const;
156+
Double_t DistFromInside_Loop(const Double_t* point, const Double_t* dir,
157+
Double_t step = TGeoShape::Big()) const;
158+
/// `Safety`'s twin over all boxes: it must equal `Safety` and be a sound bound.
159+
Double_t Safety_Loop(const Double_t* point, Bool_t in = kTRUE) const;
160+
161+
protected:
162+
/// Grow the per-cell bounding-box storage to the cell count.
163+
void EnsureCellBBoxStorage();
164+
165+
/// The accelerated DistFromOutside/DistFromInside bodies; each clips the ray to a box before using its active list.
166+
Double_t DistFromOutsideBVH(const Double_t* point, const Double_t* dir, Double_t step) const;
167+
Double_t DistFromInsideBVH(const Double_t* point, const Double_t* dir, Double_t step) const;
168+
169+
/// What the running bound prunes against: nothing, the nearest entry so far (DistFromOutside) or
170+
/// the far end of the interval holding t = 0 so far (DistFromInside).
171+
enum class RayBound { kNone,
172+
kEntry,
173+
kExit };
174+
175+
/// Each box's own occupancy pieces along the ray within `[0, step]`: `[enter, exit]` in \a pairs and its cell in \a cells, unmerged.
176+
/// False when a `CellIntervals` call overflowed. \a smallestPruned reports the nearest entry the bound skipped, Big if it skipped nothing.
177+
bool GatherRayPieces(const Double_t* point, const Double_t* dir, Double_t step,
178+
std::vector<double>& pairs, std::vector<int>& cells, RayBound bound,
179+
double& smallestPruned) const;
180+
181+
/// Recursively split `[lo, hi]` for `cell`, dropping the halfspaces the range bound decides and the boxes it proves outside.
182+
/// A split of a far-from-cubic box draws on `cubifyBudget`, any other on `depth`.
183+
void SplitBox(int cell, const double* lo, const double* hi, const std::vector<int>& active,
184+
int depth, double minSize, int cubifyBudget);
185+
186+
std::vector<FlatCSGHalfspace> fHalfspaces; ///< the flat halfspace array
187+
std::vector<FlatCSGCell> fCells; ///< the DNF's cells, indexing into it
188+
189+
/// The sub-cell boxes, rebuilt by `CloseShape`; not streamed.
190+
std::vector<FlatCSGBox> fBoxes; //!
191+
/// The boxes' active-halfspace lists, concatenated. Derived alongside `fBoxes`; not streamed
192+
/// for the same reason.
193+
std::vector<int> fActive; //!
194+
std::vector<double> fCellLo; ///< each cell's AABB low corner, 3 doubles per cell
195+
std::vector<double> fCellHi; ///< each cell's AABB high corner, 3 doubles per cell
196+
/// Whether `SetCellBBox` was ever called for a given cell; `CloseShape` refuses to build a
197+
/// solid missing one rather than silently drop that cell -- see `CloseShape`'s implementation.
198+
std::vector<bool> fCellBBoxSet;
199+
/// Set by a successful `CloseShape`; not streamed. The `#pragma read` rule closes every shape ROOT reads back.
200+
bool fClosed = false; //!
201+
/// Subdivision depth cap, and the minimum box size as a fraction of the part's bounding-box
202+
/// diagonal, chosen for query cost on the shipped parts.
203+
int fSplitDepth = 4;
204+
double fMinBoxFraction = 0.05;
205+
206+
/// The BVH over `fBoxes`, rebuilt by `CloseShape`; not streamed.
207+
void* fBVH = nullptr; //! bvh::v2::Bvh over the sub-cell boxes
208+
209+
// Scratch buffers are thread_local statics in the .cxx, never members: shapes are shared by all navigator threads.
210+
211+
ClassDefOverride(O2FlatCSG, 1) // flat-DNF halfspace shape class
212+
};
213+
214+
} // namespace cad
215+
} // namespace o2
216+
217+
#endif

Detectors/CADSupport/include/CADSupport/O2SurfaceSolidIO.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ namespace cad
2626
{
2727

2828
class O2BVHSurfaceSolid;
29+
class O2FlatCSG;
2930

3031
/// Load an exact-surface sidecar (surfaces_*.bin, versions 1-3) into \a solid through its Add*Surface methods; call CloseShape() after.
3132
/// False on an I/O or format error, when the solid may be partly filled and should be discarded.
@@ -35,6 +36,13 @@ bool LoadSurfaceSolid(const std::string& file, O2BVHSurfaceSolid& solid);
3536
/// False on an I/O or format error; degenerate facets are skipped and counted in a warning.
3637
bool LoadFacetSolid(const std::string& file, o2::base::O2Tessellated& solid);
3738

39+
/// Load a flat-CSG sidecar (flatcsg_*.bin, version 1) into \a solid; call CloseShape() after. False on an I/O or format error.
40+
bool LoadFlatCSG(const std::string& file, O2FlatCSG& solid);
41+
42+
/// Write \a solid in the same format. Used by the converter's tests and by the round-trip case;
43+
/// the production writer is Detectors/CADSupport/tools/cadsupport/flat.py, and the two must agree byte for byte.
44+
bool WriteFlatCSG(const std::string& file, const O2FlatCSG& solid);
45+
3846
} // namespace cad
3947
} // namespace o2
4048

Detectors/CADSupport/src/CADSupportLinkDef.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,12 @@
2323
#pragma link C++ class std::vector < o2::cad::BVHSurfaceRecord> + ;
2424
#pragma link C++ class o2::cad::O2BVHSurfaceSolid - ;
2525
#pragma link C++ class o2::cad::O2BVHAssembly + ;
26+
#pragma link C++ class o2::cad::FlatCSGHalfspace + ;
27+
#pragma link C++ class o2::cad::FlatCSGCell + ;
28+
#pragma link C++ class std::vector < o2::cad::FlatCSGHalfspace> + ;
29+
#pragma link C++ class std::vector < o2::cad::FlatCSGCell> + ;
30+
#pragma link C++ class o2::cad::O2FlatCSG + ;
31+
// Close every O2FlatCSG read from a file, so that any reader gets the accelerated shape.
32+
#pragma read sourceClass = "o2::cad::O2FlatCSG" targetClass = "o2::cad::O2FlatCSG" version = "[1-]" source = "" target = "" code = "{ newObj->CloseShape(); if (!newObj->IsClosed()) { newObj->Error(\"Streamer\", \"Shape %s was read from a file and CloseShape() refused it, so it has no sub-cell boxes and every query falls back to its _Loop twin. See the Error above: a cell bounding box is missing, inverted or non-finite.\", newObj->GetName()); } }";
2633

2734
#endif

0 commit comments

Comments
 (0)