Skip to content

Commit 09b12df

Browse files
sawenzelclaude
andcommitted
Replace the TPC half-space cuts by bounded boxes
This replaces the fifteen TGeoHalfSpace cuts in the TPC support geometry by equivalent bounded boxes and adds a unit test. - TGeoHalfSpace is unbounded, so ROOT's GDML writer drops every volume that uses one and VGM cannot convert it to native Geant4 geometry. - All fifteen occurrences in AliceO2 are TPC support structures, and every one of them is subtracted from a solid smaller than 10 cm in each direction. - Each is now a cube of 100 cm half-size placed with one face on the cut plane, built by the new TGeoGeometryUtils::makeHalfSpaceBox. - A subtraction takes its bounding box from the left-hand solid only, so the composite shapes are unchanged in extent. - The cut term is written in parentheses. A trailing "shape:matrix" is unsafe: TGeoManager::Parse reads the last top-level ":" of an expression that already contains a top-level ")" as a transformation of the whole expression and then drops it, which would leave an unplaced cube swallowing the parent solid. - The new test compares both forms point by point and along random rays, for the fifteen real planes, 200 arbitrary ones, and one compound expression of the shape that makes the parentheses necessary. https://its.cern.ch/jira/browse/O2-4534 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent bff2b37 commit 09b12df

5 files changed

Lines changed: 302 additions & 21 deletions

File tree

Detectors/Base/CMakeLists.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ o2_target_root_dictionary(DetectorsBase
8383
include/DetectorsBase/O2Tessellated.h
8484
)
8585

86+
o2_add_test(
87+
HalfSpaceBox
88+
SOURCES test/testHalfSpaceBox.cxx
89+
COMPONENT_NAME DetectorsBase
90+
PUBLIC_LINK_LIBRARIES O2::DetectorsBase
91+
LABELS detectorsbase)
92+
8693
if(BUILD_SIMULATION)
8794
if (NOT APPLE)
8895
o2_add_test(

Detectors/Base/include/DetectorsBase/TGeoGeometryUtils.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,20 @@ class TGeoGeometryUtils
3030
public:
3131
///< Transform any (primitive) TGeoShape to a tessellated representation
3232
static TGeoTessellated* TGeoShapeToTGeoTessellated(TGeoShape const*);
33+
34+
///< Create a bounded stand-in for the half-space { x : (x - p) . n <= 0 }, which is what
35+
///< TGeoHalfSpace describes. Registers a cube of half-size `reach` under `name` and its
36+
///< placement under "<name>_tr". The stand-in agrees with the half-space everywhere within
37+
///< a distance `reach` of `p`, so `reach` must exceed the extent of the solid it is
38+
///< subtracted from. Unlike TGeoHalfSpace, the result can be exported to GDML and
39+
///< converted to native Geant4 geometry.
40+
///<
41+
///< Write the term in a composite expression **in parentheses**, as "-(<name>:<name>_tr)".
42+
///< A trailing "shape:matrix" is not safe: TGeoManager::Parse takes the last top-level ":"
43+
///< of an expression that already contains a top-level ")" to be a transformation of the
44+
///< whole expression, warns "no geometrical transformation allowed at this level" and then
45+
///< drops it - leaving an unplaced cube at the origin that swallows the parent solid.
46+
static void makeHalfSpaceBox(const char* name, const double p[3], const double n[3], double reach);
3347
};
3448

3549
} // namespace base

Detectors/Base/src/TGeoGeometryUtils.cxx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@
1616
#include <DetectorsBase/TGeoGeometryUtils.h>
1717
#include <TGeoShape.h>
1818
#include <TGeoTessellated.h>
19+
#include <TGeoBBox.h>
20+
#include <TGeoMatrix.h>
1921
#include <TBuffer3D.h>
22+
#include <TString.h>
23+
#include <cmath>
2024
#include <vector>
2125

2226
namespace o2
@@ -140,5 +144,43 @@ TGeoTessellated* TGeoGeometryUtils::TGeoShapeToTGeoTessellated(TGeoShape const*
140144
return tes;
141145
}
142146

147+
148+
///< Bounded stand-in for a TGeoHalfSpace
149+
void TGeoGeometryUtils::makeHalfSpaceBox(const char* name, const double p[3], const double n[3], double reach)
150+
{
151+
// TGeoHalfSpace contains the points x with (p - x) . n >= 0, and normalizes n itself.
152+
double nn[3] = {n[0], n[1], n[2]};
153+
const double norm = std::sqrt(nn[0] * nn[0] + nn[1] * nn[1] + nn[2] * nn[2]);
154+
for (auto& c : nn) {
155+
c /= norm;
156+
}
157+
158+
// an orthonormal triad (u, v, nn); the seed is chosen to stay away from nn
159+
double a[3] = {1., 0., 0.};
160+
if (std::abs(nn[0]) > 0.9) {
161+
a[0] = 0.;
162+
a[1] = 1.;
163+
}
164+
double u[3] = {a[1] * nn[2] - a[2] * nn[1], a[2] * nn[0] - a[0] * nn[2], a[0] * nn[1] - a[1] * nn[0]};
165+
const double unorm = std::sqrt(u[0] * u[0] + u[1] * u[1] + u[2] * u[2]);
166+
for (auto& c : u) {
167+
c /= unorm;
168+
}
169+
const double v[3] = {nn[1] * u[2] - nn[2] * u[1], nn[2] * u[0] - nn[0] * u[2], nn[0] * u[1] - nn[1] * u[0]};
170+
171+
// rotation taking the local z axis onto nn (TGeoRotation stores the matrix row-wise,
172+
// so the images of the local axes are its columns)
173+
const double m[9] = {u[0], v[0], nn[0], u[1], v[1], nn[1], u[2], v[2], nn[2]};
174+
auto* rot = new TGeoRotation(TString::Format("%s_rot", name));
175+
rot->SetMatrix(m);
176+
177+
// centre the cube one half-size behind the plane, so its +z face lies on the plane
178+
auto* tr = new TGeoCombiTrans(TString::Format("%s_tr", name), p[0] - reach * nn[0], p[1] - reach * nn[1],
179+
p[2] - reach * nn[2], rot);
180+
tr->RegisterYourself();
181+
182+
new TGeoBBox(name, reach, reach, reach);
183+
}
184+
143185
} // namespace base
144186
} // namespace o2
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
// Copyright 2019-2020 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+
12+
/// \file testHalfSpaceBox.cxx
13+
/// \author Sandro Wenzel (CERN)
14+
/// \brief Checks that TGeoGeometryUtils::makeHalfSpaceBox reproduces TGeoHalfSpace
15+
16+
#define BOOST_TEST_MODULE Test HalfSpaceBox
17+
#define BOOST_TEST_MAIN
18+
#define BOOST_TEST_DYN_LINK
19+
#include <boost/test/unit_test.hpp>
20+
21+
#include "DetectorsBase/TGeoGeometryUtils.h"
22+
#include "TGeoManager.h"
23+
#include "TGeoBBox.h"
24+
#include "TGeoTube.h"
25+
#include "TGeoMatrix.h"
26+
#include "TGeoHalfSpace.h"
27+
#include "TGeoCompositeShape.h"
28+
#include "TMath.h"
29+
#include "TRandom3.h"
30+
#include "TString.h"
31+
#include <cmath>
32+
#include <vector>
33+
34+
namespace
35+
{
36+
struct Plane {
37+
const char* label;
38+
double p[3];
39+
double n[3];
40+
};
41+
42+
// The fifteen half-space cuts of the TPC support structures (Detectors/TPC/simulation/src/Detector.cxx).
43+
// Largest solid any of them is subtracted from is 1.65 x 1.85 x 8.9 cm, hence the 10 cm parent below.
44+
std::vector<Plane> tpcPlanes()
45+
{
46+
const double slope = TMath::Tan(22. * TMath::DegToRad());
47+
const double intp = 1.245;
48+
const double b = slope * slope + 1.;
49+
const double p1[3] = {intp * slope / b, -intp / b, 0.};
50+
const double p2[3] = {-intp * slope / b, -intp / b, 0.};
51+
return {
52+
{"sp1", {p1[0], p1[1], 0.}, {-p1[0], -p1[1], 0.}},
53+
{"sp2", {p2[0], p2[1], 0.}, {-p2[0], -p2[1], 0.}},
54+
{"cutil1", {0., 0.105, 0.}, {0., 1., 0.}},
55+
{"cutomh1", {0., -1.05, -3.4}, {0., -TMath::Tan(30. * TMath::DegToRad()), 1.}},
56+
{"cutomh2", {0., -1.05, 3.4}, {0., -TMath::Tan(30. * TMath::DegToRad()), -1.}},
57+
{"cutomh3", {-1.65, 0., -0.9}, {TMath::Tan(75. * TMath::DegToRad()), 0., 1.}},
58+
{"cutomh4", {-1.65, 0., 0.9}, {TMath::Tan(75. * TMath::DegToRad()), 0., -1.}},
59+
{"cutomh5", {1.65, -1.05, 0.}, {-1., -TMath::Tan(20. * TMath::DegToRad()), 0.}},
60+
{"cutohs1", {0., -0.186, 0.}, {0., -1., 0.}},
61+
{"cutmmh1", {-1.65, 0., -0.9}, {8., 0., 8. * TMath::Tan(13. * TMath::DegToRad())}},
62+
{"cutmmh2", {-1.65, 0., 0.9}, {8., 0., -8. * TMath::Tan(13. * TMath::DegToRad())}},
63+
{"cutmmh3", {0., 1.85, -2.8}, {0., -6.1, 6.1 * TMath::Tan(20. * TMath::DegToRad())}},
64+
{"cutmmh4", {0., 1.85, 2.8}, {0., -6.1, -6.1 * TMath::Tan(20. * TMath::DegToRad())}},
65+
{"cutmmh5", {0.75, 0., -8.9}, {2.4 * TMath::Tan(30. * TMath::DegToRad()), 0., 2.4}},
66+
{"cutmmh6", {0.75, 0., 8.9}, {2.4 * TMath::Tan(30. * TMath::DegToRad()), 0., -2.4}}};
67+
}
68+
69+
// Compares "parent - halfspace" against "parent - box:box_tr" on random points and random rays.
70+
// Points closer than kSurfaceBand to the plane are skipped: on the surface itself the two
71+
// implementations may legitimately round to different sides.
72+
void compare(const TString& tag, const double p[3], const double n[3], double parentHalfSize, double reach,
73+
TRandom3& rnd, int nPoints, int nRays, double& maxDistDiff)
74+
{
75+
const double nl = std::sqrt(n[0] * n[0] + n[1] * n[1] + n[2] * n[2]);
76+
BOOST_REQUIRE(nl > 1e-6);
77+
constexpr double kSurfaceBand = 1e-9;
78+
79+
new TGeoBBox(TString::Format("parent_%s", tag.Data()).Data(), parentHalfSize, parentHalfSize, parentHalfSize);
80+
new TGeoHalfSpace(TString::Format("hs_%s", tag.Data()).Data(), const_cast<double*>(p), const_cast<double*>(n));
81+
o2::base::TGeoGeometryUtils::makeHalfSpaceBox(TString::Format("bx_%s", tag.Data()).Data(), p, n, reach);
82+
83+
auto* ref = new TGeoCompositeShape(TString::Format("ref_%s", tag.Data()),
84+
TString::Format("parent_%s-hs_%s", tag.Data(), tag.Data()));
85+
auto* box = new TGeoCompositeShape(TString::Format("new_%s", tag.Data()),
86+
TString::Format("parent_%s-(bx_%s:bx_%s_tr)", tag.Data(), tag.Data(), tag.Data()));
87+
88+
const double range = 1.2 * parentHalfSize;
89+
for (int k = 0; k < nPoints; ++k) {
90+
double x[3];
91+
for (int i = 0; i < 3; ++i) {
92+
x[i] = rnd.Uniform(-range, range);
93+
}
94+
const double d = ((x[0] - p[0]) * n[0] + (x[1] - p[1]) * n[1] + (x[2] - p[2]) * n[2]) / nl;
95+
if (std::abs(d) < kSurfaceBand) {
96+
continue;
97+
}
98+
if (ref->Contains(x) != box->Contains(x)) {
99+
BOOST_REQUIRE_MESSAGE(false, "containment differs for " << tag.Data() << " at (" << x[0] << "," << x[1] << ","
100+
<< x[2] << "), distance to plane " << d);
101+
}
102+
}
103+
104+
for (int k = 0; k < nRays; ++k) {
105+
double x[3], dir[3];
106+
for (int i = 0; i < 3; ++i) {
107+
x[i] = rnd.Uniform(-3. * range, 3. * range);
108+
dir[i] = rnd.Uniform(-1., 1.);
109+
}
110+
const double dn = std::sqrt(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]);
111+
if (dn < 1e-6) {
112+
continue;
113+
}
114+
for (int i = 0; i < 3; ++i) {
115+
dir[i] /= dn;
116+
}
117+
const bool inside = ref->Contains(x);
118+
if (inside != box->Contains(x)) {
119+
continue; // a point sitting on the surface; covered by the containment loop above
120+
}
121+
const double d1 = inside ? ref->DistFromInside(x, dir, 3) : ref->DistFromOutside(x, dir, 3);
122+
const double d2 = inside ? box->DistFromInside(x, dir, 3) : box->DistFromOutside(x, dir, 3);
123+
if (d1 > 1e15 && d2 > 1e15) {
124+
continue; // both miss
125+
}
126+
maxDistDiff = std::max(maxDistDiff, std::abs(d1 - d2));
127+
}
128+
}
129+
} // namespace
130+
131+
BOOST_AUTO_TEST_CASE(HalfSpaceBox_reproduces_TGeoHalfSpace)
132+
{
133+
auto* geom = new TGeoManager("halfspacetest", "half-space replacement test");
134+
TRandom3 rnd(20240101);
135+
double maxDistDiff = 0.;
136+
137+
// the real TPC cuts
138+
for (const auto& pl : tpcPlanes()) {
139+
compare(pl.label, pl.p, pl.n, 10., 100., rnd, 200000, 20000, maxDistDiff);
140+
}
141+
142+
// and a spread of arbitrary planes, to pin the rotation for normals in every octant
143+
for (int i = 0; i < 200; ++i) {
144+
double p[3], n[3];
145+
for (int k = 0; k < 3; ++k) {
146+
p[k] = rnd.Uniform(-5., 5.);
147+
n[k] = rnd.Uniform(-1., 1.);
148+
}
149+
if (std::sqrt(n[0] * n[0] + n[1] * n[1] + n[2] * n[2]) < 1e-3) {
150+
continue;
151+
}
152+
compare(TString::Format("rnd%d", i), p, n, 10., 100., rnd, 20000, 2000, maxDistDiff);
153+
}
154+
155+
// the two shapes are not bit-identical, but they must agree to double round-off
156+
BOOST_CHECK_SMALL(maxDistDiff, 1e-9);
157+
BOOST_TEST_MESSAGE("maximum ray-distance difference: " << maxDistDiff);
158+
delete geom;
159+
}
160+
161+
// The composite expressions of the TPC support structures are not all of the simple
162+
// "parent - cut" shape: tpcihs6 subtracts a union and two placed tubes first. That shape is
163+
// what makes a *trailing* "cut:matrix" term unsafe to write unparenthesised, so keep a case
164+
// with the same structure.
165+
BOOST_AUTO_TEST_CASE(HalfSpaceBox_in_a_compound_expression)
166+
{
167+
auto* geom = new TGeoManager("halfspacetest2", "half-space replacement, compound expression");
168+
const double shift[3] = {0., -0.175, 0.};
169+
const double p[3] = {0., 0.105, 0.};
170+
const double n[3] = {0., 1., 0.};
171+
172+
new TGeoBBox("tpcihs1", 4.7, 0.66, 2.35);
173+
new TGeoBBox("tpcihs2", 4.7, 0.485, 1.0, const_cast<double*>(shift));
174+
new TGeoBBox("tpcihs3", 1.5, 0.485, 2.35, const_cast<double*>(shift));
175+
new TGeoTube("tpcihs4", 0.0, 2.38, 0.1);
176+
auto* trans2 = new TGeoTranslation("trans2", 0.0, 2.84, 2.25);
177+
trans2->RegisterYourself();
178+
auto* trans3 = new TGeoTranslation("trans3", 0.0, 2.84, -2.25);
179+
trans3->RegisterYourself();
180+
new TGeoHalfSpace("cutil1", const_cast<double*>(p), const_cast<double*>(n));
181+
o2::base::TGeoGeometryUtils::makeHalfSpaceBox("bcutil1", p, n, 100.);
182+
183+
auto* ref = new TGeoCompositeShape(
184+
"ref_tpcihs6", "tpcihs1-(tpcihs2+tpcihs3)-(tpcihs4:trans2)-(tpcihs4:trans3)-cutil1");
185+
auto* box = new TGeoCompositeShape(
186+
"new_tpcihs6", "tpcihs1-(tpcihs2+tpcihs3)-(tpcihs4:trans2)-(tpcihs4:trans3)-(bcutil1:bcutil1_tr)");
187+
188+
TRandom3 rnd(20240102);
189+
long inRef = 0, inBox = 0;
190+
for (int k = 0; k < 2000000; ++k) {
191+
double x[3];
192+
for (int i = 0; i < 3; ++i) {
193+
x[i] = rnd.Uniform(-6., 6.);
194+
}
195+
if (std::abs(x[1] - p[1]) < 1e-9) {
196+
continue;
197+
}
198+
const bool a = ref->Contains(x);
199+
const bool b = box->Contains(x);
200+
inRef += a;
201+
inBox += b;
202+
if (a != b) {
203+
BOOST_REQUIRE_MESSAGE(false, "containment differs at (" << x[0] << "," << x[1] << "," << x[2] << ")");
204+
}
205+
}
206+
// guards against both shapes being empty, which would make the comparison vacuous
207+
BOOST_CHECK_GT(inRef, 0);
208+
BOOST_CHECK_EQUAL(inRef, inBox);
209+
delete geom;
210+
}

0 commit comments

Comments
 (0)