Skip to content

Commit 9ae3442

Browse files
sawenzelclaude
andcommitted
Add a VecGeom navigation mode to the Geant4 engine
This adds G4.navmode=kVecGeom, in which Geant4 answers every navigation query from VecGeom. - The geometry, materials and touchables stay the ones g4root builds from TGeo. - VecGeomG4Map pairs each VecGeom placement with the chain of g4root volumes it stands for, so volume ids, copy numbers and CurrentVolOffID are unchanged. - VecGeomG4Navigator relocates at the boundary locate, with the volume just left blocked for the next step and the point pushed across the face by a small depth, as G4VecGeomNav's TG4VecGeomNavigator does. - G4.vecgeomCheckRays, vecgeomCheckLocation and vecgeomCheckVolumes compare VecGeom with TGeo before transport. - It is built only when TGeo2VecGeom and a VecGeom with BVHNavigatorV are found. https://gitlab.cern.ch/VecGeom/g4vecgeomnav/-/merge_requests/25 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 09cd96e commit 9ae3442

13 files changed

Lines changed: 1626 additions & 4 deletions

‎Common/SimConfig/include/SimConfig/G4Params.h‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@ enum class EG4Physics {
3636
// enumerating possible geometry navigation modes
3737
// (understanding that geometry description is always done with TGeo)
3838
enum class EG4Nav {
39-
kTGeo = 0, /* navigate with TGeo */
40-
kG4 = 1 /* navigate with G4 native geometry */
39+
kTGeo = 0, /* navigate with TGeo */
40+
kG4 = 1, /* navigate with G4 native geometry */
41+
kVecGeom = 2 /* navigate with VecGeom, on the G4 geometry built from TGeo */
4142
};
4243

4344
// parameters to influence the G4 engine
@@ -49,6 +50,20 @@ struct G4Params : public o2::conf::ConfigurableParamHelper<G4Params> {
4950

5051
EG4Nav navmode = EG4Nav::kTGeo; // geometry navigation mode (default TGeo)
5152

53+
// Settings for navmode == kVecGeom; ignored otherwise.
54+
double vecgeomPushDepth = 1.e-9; // cm; how far past a face, measured across it, a boundary
55+
// point is pushed before it is located
56+
bool vecgeomZeroSafety = false; // answer zero to every safety query; conservative, but it
57+
// shortens steps and so changes the random history
58+
bool vecgeomFlattenAssemblies = true; // dissolve TGeo assemblies into their content when converting
59+
// to VecGeom; the Geant4 touchable keeps the assembly levels
60+
int vecgeomCheckRays = 0; // if > 0, step this many rays out of the interaction point with
61+
// TGeo and VecGeom and report the volumes they enter differently
62+
int vecgeomCheckLocation = 0; // if > 0, locate this many random points with both and report
63+
// the volumes they disagree on
64+
std::string vecgeomCheckVolumes = ""; // comma-separated volumes to cross-check by sampling inside
65+
// their placements
66+
5267
std::string fluenceWeightFile = ""; // file containing the scoring weights (pdg, ekin, weight)
5368
std::string const& getPhysicsConfigString() const;
5469

‎Detectors/gconfig/CMakeLists.txt‎

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,39 @@ o2_add_library(G3Setup
1414
PUBLIC_LINK_LIBRARIES MC::Geant3 FairRoot::Base O2::SimulationDataFormat O2::Generators O2::SimSetup
1515
)
1616

17+
# Optional VecGeom navigation for Geant4 (G4.navmode=kVecGeom). It needs TGeo2VecGeom and a VecGeom
18+
# with the BVH navigator of the VNavigator family (BVHNavigatorV). Linked PRIVATE: VecGeom types
19+
# never appear in G4Setup's public headers.
20+
find_package(TGeo2VecGeom CONFIG QUIET)
21+
set(G4SETUP_WITH_VECGEOM OFF)
22+
if(TGeo2VecGeom_FOUND)
23+
find_path(O2_VECGEOM_BVHNAVIGATORV_INCLUDE VecGeom/navigation/BVHNavigatorV.h
24+
HINTS ${VecGeom_INCLUDE_DIRS} ${VecGeom_DIR}/../../../include $ENV{VECGEOM_ROOT}/include)
25+
if(O2_VECGEOM_BVHNAVIGATORV_INCLUDE)
26+
set(G4SETUP_WITH_VECGEOM ON)
27+
endif()
28+
endif()
29+
30+
set(G4SETUP_SOURCES src/G4Config.cxx src/G4RunConfiguration.cxx src/G4LocalFieldConstruction.cxx
31+
src/VecGeomNavigation.cxx)
32+
if(G4SETUP_WITH_VECGEOM)
33+
list(APPEND G4SETUP_SOURCES src/VecGeomG4Map.cxx src/VecGeomChecks.cxx src/VecGeomG4NavigatorBase.cxx
34+
src/VecGeomG4Navigator.cxx)
35+
endif()
36+
1737
o2_add_library(G4Setup
18-
SOURCES src/G4Config.cxx src/G4RunConfiguration.cxx src/G4LocalFieldConstruction.cxx
19-
PUBLIC_LINK_LIBRARIES MC::Geant4VMC MC::Geant4 FairRoot::Base O2::SimulationDataFormat O2::Generators O2::SimSetup O2::FastSim
38+
TARGETVARNAME targetG4Setup
39+
SOURCES ${G4SETUP_SOURCES}
40+
PUBLIC_LINK_LIBRARIES MC::Geant4VMC MC::Geant4 FairRoot::Base O2::SimulationDataFormat O2::Generators O2::SimSetup O2::FastSim O2::DetectorsBase
2041
)
2142

43+
if(G4SETUP_WITH_VECGEOM)
44+
target_compile_definitions(${targetG4Setup} PRIVATE O2_WITH_VECGEOM)
45+
target_link_libraries(${targetG4Setup} PRIVATE TGeo2VecGeom::TGeo2VecGeom)
46+
else()
47+
message(STATUS "G4.navmode=kVecGeom not built: it needs TGeo2VecGeom and a VecGeom with BVHNavigatorV")
48+
endif()
49+
2250
o2_add_library(FLUKASetup
2351
SOURCES src/FlukaConfig.cxx
2452
PUBLIC_LINK_LIBRARIES FairRoot::Base O2::SimulationDataFormat O2::Generators O2::SimSetup

‎Detectors/gconfig/g4Config.C‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ R__LOAD_LIBRARY(libgeant4vmc)
6666
#include "G4VScoringMesh.hh"
6767
#include <unistd.h>
6868
#include "SimSetup/G4RunConfiguration.h"
69+
#include "SimSetup/VecGeomNavigation.h"
6970
#endif
7071
#include "commonConfig.C"
7172

@@ -115,6 +116,10 @@ void Config()
115116
geomNavStr = "geomRoot";
116117
} else if (g4Params.navmode == o2::conf::EG4Nav::kG4) {
117118
geomNavStr = "geomVMC+RootToGeant4";
119+
} else if (g4Params.navmode == o2::conf::EG4Nav::kVecGeom) {
120+
// The geometry, its materials and the touchable stay the ones g4root builds from TGeo;
121+
// only the navigator is swapped, once the engine below has built that hierarchy.
122+
geomNavStr = "geomRoot";
118123
} else {
119124
LOG(fatal) << "Unsupported geometry navigation mode";
120125
}
@@ -137,6 +142,10 @@ void Config()
137142
TGeant4* geant4 = new TGeant4("TGeant4", "The Geant4 Monte Carlo", runConfiguration);
138143
std::cout << "Geant4 has been created." << std::endl;
139144

145+
if (g4Params.navmode == o2::conf::EG4Nav::kVecGeom) {
146+
o2::simsetup::installVecGeomNavigator();
147+
}
148+
140149
// setup the stack
141150
stackSetup(geant4, FairRunSim::Instance());
142151

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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+
#ifndef O2_SIMSETUP_VECGEOMNAVIGATION_H_
13+
#define O2_SIMSETUP_VECGEOMNAVIGATION_H_
14+
15+
namespace o2::simsetup
16+
{
17+
18+
/// Whether this build of O2 has the VecGeom navigation backend, i.e. whether TGeo2VecGeom and a
19+
/// VecGeom with BVHNavigatorV were found when O2 was configured.
20+
bool isVecGeomNavigationAvailable();
21+
22+
/// Replaces Geant4's tracking navigator by one that answers every navigation query from
23+
/// VecGeom. The Geant4 geometry, its materials and the touchable the scoring code reads stay
24+
/// the ones g4root built from TGeo, so only navigation changes.
25+
///
26+
/// Call after the TGeant4 engine has been constructed: the Geant4 hierarchy this needs to map
27+
/// onto is built while TG4RunManager configures itself. Aborts if the backend is missing.
28+
void installVecGeomNavigator();
29+
30+
} // namespace o2::simsetup
31+
32+
#endif
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
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+
#include "VecGeomChecks.h"
13+
14+
#include "TGeoBBox.h"
15+
#include "TGeoManager.h"
16+
#include "TGeoMatrix.h"
17+
#include "TGeoNode.h"
18+
#include "TGeoVolume.h"
19+
#include "TRandom3.h"
20+
21+
#include <VecGeom/base/Transformation3D.h>
22+
#include <VecGeom/management/GeoManager.h>
23+
#include <VecGeom/navigation/GlobalLocator.h>
24+
#include <VecGeom/navigation/NavigationState.h>
25+
#include <VecGeom/navigation/VNavigator.h>
26+
#include <VecGeom/volumes/LogicalVolume.h>
27+
#include <VecGeom/volumes/PlacedVolume.h>
28+
29+
#include <fairlogger/Logger.h>
30+
31+
#include <algorithm>
32+
#include <cmath>
33+
#include <map>
34+
#include <string>
35+
#include <utility>
36+
#include <vector>
37+
38+
using V3 = vecgeom::Vector3D<double>;
39+
40+
namespace o2::simsetup
41+
{
42+
43+
std::size_t checkVecGeomLocation(std::size_t samples)
44+
{
45+
auto* world = vecgeom::GeoManager::Instance().GetWorld();
46+
auto const* box = dynamic_cast<TGeoBBox const*>(gGeoManager->GetTopVolume()->GetShape());
47+
if (world == nullptr || box == nullptr) {
48+
LOG(warning) << "Cannot cross-check the VecGeom location: no world";
49+
return 0;
50+
}
51+
TRandom3 rnd(12345);
52+
vecgeom::NavigationState state;
53+
std::map<std::string, std::size_t> byVolume;
54+
std::size_t bad = 0;
55+
for (std::size_t i = 0; i < samples; ++i) {
56+
const double x = box->GetOrigin()[0] + box->GetDX() * (2. * rnd.Rndm() - 1.);
57+
const double y = box->GetOrigin()[1] + box->GetDY() * (2. * rnd.Rndm() - 1.);
58+
const double z = box->GetOrigin()[2] + box->GetDZ() * (2. * rnd.Rndm() - 1.);
59+
auto* node = gGeoManager->FindNode(x, y, z);
60+
const std::string tgeoName = (node != nullptr) ? node->GetVolume()->GetName() : "<outside>";
61+
state.Clear();
62+
vecgeom::GlobalLocator::LocateGlobalPoint(world, V3(x, y, z), state, true);
63+
auto const* top = state.Top();
64+
const std::string vgName = (top != nullptr) ? top->GetLogicalVolume()->GetName() : "<outside>";
65+
if (tgeoName != vgName) {
66+
++bad;
67+
++byVolume[tgeoName + " -> " + vgName];
68+
}
69+
}
70+
LOG(info) << "VecGeom location cross-check: " << bad << " of " << samples << " points land in a "
71+
<< "different volume than TGeo puts them in";
72+
std::vector<std::pair<std::size_t, std::string>> worst;
73+
for (auto const& e : byVolume) {
74+
worst.emplace_back(e.second, e.first);
75+
}
76+
std::sort(worst.rbegin(), worst.rend());
77+
for (std::size_t i = 0; i < worst.size() && i < 15; ++i) {
78+
LOG(info) << " " << worst[i].first << " " << worst[i].second;
79+
}
80+
return bad;
81+
}
82+
83+
std::size_t checkVecGeomVolume(const char* name, std::size_t perPlacement, std::size_t maxPlacements)
84+
{
85+
auto* world = vecgeom::GeoManager::Instance().GetWorld();
86+
auto* vol = gGeoManager->GetVolume(name);
87+
if (world == nullptr || vol == nullptr) {
88+
LOG(warning) << "Cannot cross-check volume " << name << ": not in the geometry";
89+
return 0;
90+
}
91+
auto const* box = dynamic_cast<TGeoBBox const*>(vol->GetShape());
92+
if (box == nullptr) {
93+
LOG(warning) << "Cannot cross-check volume " << name << ": its shape has no bounding box";
94+
return 0;
95+
}
96+
LOG(info) << "VecGeom sizes: " << vecgeom::GeoManager::Instance().GetRegisteredVolumesCount()
97+
<< " logical, " << vecgeom::GeoManager::Instance().GetPlacedVolumesCount() << " placed, "
98+
<< vecgeom::VPlacedVolume::GetIdCount() << " ids handed out";
99+
// Divisions and other generated placements are where the two trees are most likely to differ
100+
// in shape rather than in position, so report the daughter counts before sampling anything.
101+
if (auto* lv = vecgeom::GeoManager::Instance().FindLogicalVolume(name)) {
102+
LOG(info) << " " << name << ": TGeo " << vol->GetNdaughters() << " daughters, VecGeom "
103+
<< lv->GetDaughters().size();
104+
}
105+
{
106+
TIter nextVol(gGeoManager->GetListOfVolumes());
107+
TGeoVolume* mother = nullptr;
108+
while ((mother = static_cast<TGeoVolume*>(nextVol())) != nullptr) {
109+
bool found = false;
110+
for (int i = 0; i < mother->GetNdaughters(); ++i) {
111+
if (mother->GetNode(i)->GetVolume() == vol) {
112+
found = true;
113+
break;
114+
}
115+
}
116+
if (!found) {
117+
continue;
118+
}
119+
auto* mlv = vecgeom::GeoManager::Instance().FindLogicalVolume(mother->GetName());
120+
LOG(info) << " mother " << mother->GetName() << ": TGeo " << mother->GetNdaughters()
121+
<< " daughters, VecGeom " << (mlv != nullptr ? (long)mlv->GetDaughters().size() : -1)
122+
<< (mother->GetFinder() != nullptr ? " (divided)" : "");
123+
break;
124+
}
125+
}
126+
TRandom3 rnd(4321);
127+
vecgeom::NavigationState state;
128+
std::map<std::string, std::size_t> byResult;
129+
std::size_t placements = 0, tested = 0, bad = 0, badTransform = 0;
130+
double worstTransform = 0.;
131+
TGeoIterator it(gGeoManager->GetTopVolume());
132+
TGeoNode* node = nullptr;
133+
while ((node = it.Next()) != nullptr && placements < maxPlacements) {
134+
if (node->GetVolume() != vol) {
135+
continue;
136+
}
137+
++placements;
138+
TGeoHMatrix matrix = *it.GetCurrentMatrix();
139+
for (std::size_t k = 0; k < perPlacement; ++k) {
140+
double local[3] = {box->GetOrigin()[0] + box->GetDX() * (2. * rnd.Rndm() - 1.),
141+
box->GetOrigin()[1] + box->GetDY() * (2. * rnd.Rndm() - 1.),
142+
box->GetOrigin()[2] + box->GetDZ() * (2. * rnd.Rndm() - 1.)};
143+
if (!vol->GetShape()->Contains(local)) {
144+
continue;
145+
}
146+
double global[3];
147+
matrix.LocalToMaster(local, global);
148+
++tested;
149+
auto* found = gGeoManager->FindNode(global[0], global[1], global[2]);
150+
const std::string tgeoName = (found != nullptr) ? found->GetVolume()->GetName() : "<outside>";
151+
state.Clear();
152+
vecgeom::GlobalLocator::LocateGlobalPoint(world, V3(global[0], global[1], global[2]), state, true);
153+
auto const* top = state.Top();
154+
const std::string vgName = (top != nullptr) ? top->GetLogicalVolume()->GetName() : "<outside>";
155+
if (tgeoName != vgName) {
156+
++bad;
157+
++byResult[tgeoName + " -> " + vgName];
158+
continue;
159+
}
160+
// The volume is right; check that the state also composes the right transform. That goes
161+
// through the navigation index table, which is built separately from the daughter lists
162+
// the locators use, so it can be wrong where containment looks perfect.
163+
vecgeom::Transformation3D trans;
164+
state.TopMatrix(trans);
165+
const auto vgLocal = trans.Transform(V3(global[0], global[1], global[2]));
166+
const double d = std::sqrt((vgLocal[0] - local[0]) * (vgLocal[0] - local[0]) +
167+
(vgLocal[1] - local[1]) * (vgLocal[1] - local[1]) +
168+
(vgLocal[2] - local[2]) * (vgLocal[2] - local[2]));
169+
worstTransform = std::max(worstTransform, d);
170+
if (d > 1.e-6) {
171+
++badTransform;
172+
}
173+
}
174+
}
175+
LOG(info) << "VecGeom volume cross-check " << name << ": worst local-point deviation "
176+
<< worstTransform << " cm, " << badTransform << " points above 1e-6 cm";
177+
LOG(info) << "VecGeom volume cross-check " << name << ": " << placements << " placements, " << tested
178+
<< " points, " << bad << " located differently than TGeo";
179+
for (auto const& e : byResult) {
180+
LOG(info) << " " << e.second << " " << e.first;
181+
}
182+
return bad;
183+
}
184+
185+
void checkVecGeomRays(std::size_t rays)
186+
{
187+
auto* world = vecgeom::GeoManager::Instance().GetWorld();
188+
if (world == nullptr) {
189+
return;
190+
}
191+
TRandom3 rnd(97531);
192+
std::map<std::string, std::size_t> tgeoSeen, vgSeen;
193+
constexpr std::size_t kMaxSteps = 20000;
194+
195+
for (std::size_t r = 0; r < rays; ++r) {
196+
const double cost = 2. * rnd.Rndm() - 1.;
197+
const double sint = std::sqrt(1. - cost * cost);
198+
const double phi = 2. * M_PI * rnd.Rndm();
199+
const double dir[3] = {sint * std::cos(phi), sint * std::sin(phi), cost};
200+
201+
gGeoManager->InitTrack(0., 0., 0., dir[0], dir[1], dir[2]);
202+
for (std::size_t k = 0; k < kMaxSteps && !gGeoManager->IsOutside(); ++k) {
203+
++tgeoSeen[gGeoManager->GetCurrentVolume()->GetName()];
204+
gGeoManager->FindNextBoundaryAndStep();
205+
}
206+
207+
vecgeom::NavigationState cur, next;
208+
V3 pos(0., 0., 0.);
209+
const V3 vdir(dir[0], dir[1], dir[2]);
210+
vecgeom::GlobalLocator::LocateGlobalPoint(world, pos, cur, true);
211+
for (std::size_t k = 0; k < kMaxSteps && cur.Top() != nullptr; ++k) {
212+
++vgSeen[cur.Top()->GetLogicalVolume()->GetName()];
213+
double safety = 0.;
214+
auto const* nav = cur.Top()->GetLogicalVolume()->GetNavigator();
215+
const double step = nav->ComputeStepAndSafetyAndPropagatedState(pos, vdir, vecgeom::kInfLength, cur, next,
216+
false, safety);
217+
if (!(step < vecgeom::kInfLength)) {
218+
break;
219+
}
220+
pos = pos + step * vdir;
221+
cur = next;
222+
}
223+
}
224+
225+
// A volume one engine never enters is the sharpest signal: a hit can only be made in a
226+
// volume a track actually reaches, so these are the ones that lose a detector its hits.
227+
std::vector<std::string> neverVG, neverTGeo;
228+
std::vector<std::pair<long, std::string>> diff;
229+
for (auto const& e : tgeoSeen) {
230+
const std::size_t vg = vgSeen.count(e.first) ? vgSeen[e.first] : 0;
231+
if (vg == 0) {
232+
neverVG.push_back(e.first);
233+
}
234+
const long d = static_cast<long>(e.second) - static_cast<long>(vg);
235+
if (d != 0) {
236+
diff.emplace_back(std::labs(d), e.first);
237+
}
238+
}
239+
for (auto const& e : vgSeen) {
240+
if (tgeoSeen.count(e.first) == 0) {
241+
neverTGeo.push_back(e.first);
242+
}
243+
}
244+
std::sort(diff.rbegin(), diff.rend());
245+
LOG(info) << "VecGeom ray cross-check over " << rays << " rays: " << tgeoSeen.size() << " volumes seen by TGeo, "
246+
<< vgSeen.size() << " by VecGeom, " << diff.size() << " entered a different number of times";
247+
LOG(info) << " never entered by VecGeom (" << neverVG.size() << "):";
248+
for (std::size_t i = 0; i < neverVG.size() && i < 60; ++i) {
249+
LOG(info) << " " << neverVG[i] << " (TGeo " << tgeoSeen[neverVG[i]] << ")";
250+
}
251+
LOG(info) << " never entered by TGeo (" << neverTGeo.size() << "):";
252+
for (std::size_t i = 0; i < neverTGeo.size() && i < 30; ++i) {
253+
LOG(info) << " " << neverTGeo[i] << " (VecGeom " << vgSeen[neverTGeo[i]] << ")";
254+
}
255+
}
256+
257+
} // namespace o2::simsetup

0 commit comments

Comments
 (0)