Skip to content

Commit e2da778

Browse files
committed
CAD->TGeo: support multiple external modules and sensitive detectors
Improve the CAD->TGeo integration path for o2-sim with a configurable JSON-driven setup for externally provided CAD geometry. The configuration can describe multiple passive external modules and sensitive external detectors, both activated through the normal detector/module list. Add a --extGeomFile option carrying the external geometry configuration. Passive CAD modules no longer need to be hardcoded in build_geometry.C, and several configured CAD modules can be injected in one simulation. Introduce shared CAD geometry utilities to JIT-load O2_CADtoTGeo.py-style ROOT macros into unique namespaces, allowing several CAD modules to coexist in one Cling session despite identical exported builder symbols. The utilities also remap imported CAD media into the O2 MaterialManager. Add o2::ext::ExternalDetector, a generic sensitive detector implementation for externally supplied CAD geometry. Configured volumes or media can be marked sensitive, the detector is tied to a free DetID slot, and hits flow through the standard o2-sim forwarding and merging machinery. Add o2::ext::Hit as a detector-agnostic external hit payload storing entrance/exit position, momentum, energy, energy loss, time, length, PDG code and track status flags. All external detector instances share this wire format, so the hit merger only needs one external hit type. Support optional per-detector sensitive actions loaded from ROOT macros at runtime via GetFromMacro. When no custom action is configured, a built-in charged-track entrance/exit action is used. Register active external detectors in O2HitMerger so their hits are persisted in parallel mode, and key DetImpl hit collection buffers by detector instance to support multiple detectors sharing the same C++ type. Add a self-contained SimExample with two artificial sensitive external detectors, demonstrating configurable geometry injection, built-in and custom sensitive actions, and parallel-mode hit persistence.
1 parent 630c8fb commit e2da778

30 files changed

Lines changed: 1853 additions & 166 deletions

.clang-format

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,7 @@ UseTab: Never
5454
# Do not format protobuf files
5555
Language: Proto
5656
DisableFormat: true
57+
---
58+
# Do not format JSON configuration files
59+
Language: Json
60+
DisableFormat: true

Common/SimConfig/include/SimConfig/SimConfig.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,9 @@ struct SimConfigData {
8888
bool mForwardKine = false; // true if tracks and event headers are to be published on a FairMQ channel (for reading by other consumers)
8989
bool mWriteToDisc = true; // whether we write simulation products (kine, hits) to disc
9090
VertexMode mVertexMode = VertexMode::kDiamondParam; // by default we should use die InteractionDiamond parameter
91+
std::string mExtGeomFile = ""; // optional path to a JSON file describing external (CAD) geometry modules to inject
9192

92-
ClassDefNV(SimConfigData, 4);
93+
ClassDefNV(SimConfigData, 5);
9394
};
9495

9596
// A singleton class which can be used
@@ -178,6 +179,7 @@ class SimConfig
178179
bool forwardKine() const { return mConfigData.mForwardKine; }
179180
bool writeToDisc() const { return mConfigData.mWriteToDisc; }
180181
VertexMode getVertexMode() const { return mConfigData.mVertexMode; }
182+
std::string getExtGeomFilename() const { return mConfigData.mExtGeomFile; }
181183

182184
// returns the pair of collision context filename as well as event prefix encoded
183185
// in the mFromCollisionContext string. Returns empty string if information is not available or set.

Common/SimConfig/src/SimConfig.cxx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ void SimConfig::initOptions(boost::program_options::options_description& options
7575
"asservice", bpo::value<bool>()->default_value(false), "run in service/server mode")(
7676
"noGeant", bpo::bool_switch(), "prohibits any Geant transport/physics (by using tight cuts)")(
7777
"forwardKine", bpo::bool_switch(), "forward kinematics on a FairMQ channel")(
78-
"noDiscOutput", bpo::bool_switch(), "switch off writing sim results to disc (useful in combination with forwardKine)");
78+
"noDiscOutput", bpo::bool_switch(), "switch off writing sim results to disc (useful in combination with forwardKine)")(
79+
"extGeomFile", bpo::value<std::string>()->default_value(""), "Path to a JSON file describing external (CAD) geometry modules to inject (see Detectors/Passive ExternalModule). Modules are added when their 'name' is part of the active module list.");
7980
options.add_options()("fromCollContext", bpo::value<std::string>()->default_value(""), "Use a pregenerated collision context to infer number of events to simulate, how to embedd them, the vertex position etc. Takes precedence of other options such as \"--nEvents\". The format is COLLISIONCONTEXTFILE.root[:SIGNALNAME] where SIGNALNAME is the event part in the context which is relevant.");
8081
}
8182

@@ -354,6 +355,7 @@ bool SimConfig::resetFromParsedMap(boost::program_options::variables_map const&
354355
if (vm.count("noemptyevents")) {
355356
mConfigData.mFilterNoHitEvents = true;
356357
}
358+
mConfigData.mExtGeomFile = vm["extGeomFile"].as<std::string>();
357359
mConfigData.mFromCollisionContext = vm["fromCollContext"].as<std::string>();
358360
auto collcontext_simprefix = getCollContextFilenameAndEventPrefix();
359361
adjustFromCollContext(collcontext_simprefix.first, collcontext_simprefix.second);

Detectors/Base/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ o2_add_library(DetectorsBase
3131
src/GlobalParams.cxx
3232
src/O2Tessellated.cxx
3333
src/TGeoGeometryUtils.cxx
34+
src/CADGeometryUtils.cxx
3435
PUBLIC_LINK_LIBRARIES FairRoot::Base
3536
O2::CommonUtils
3637
O2::DetectorsCommonDataFormats
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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 CADGeometryUtils.h
13+
/// \brief Helpers to inject CAD-derived (TGeo) geometry into O2 simulation
14+
///
15+
/// These utilities are shared between purely passive external modules
16+
/// (o2::passive::ExternalModule) and sensitive external detectors
17+
/// (o2::ext::ExternalDetector). They deal with the geometry produced by
18+
/// scripts/geometry/O2_CADtoTGeo.py, which is emitted as a ROOT macro.
19+
20+
#ifndef ALICEO2_BASE_CADGEOMETRYUTILS_H
21+
#define ALICEO2_BASE_CADGEOMETRYUTILS_H
22+
23+
#include <string>
24+
25+
class TGeoVolume;
26+
27+
namespace o2::base
28+
{
29+
30+
/// JIT-compile a CAD-derived ROOT geometry macro (as produced by O2_CADtoTGeo.py)
31+
/// and execute it to obtain the top TGeoVolume of the described module.
32+
///
33+
/// The macro body is wrapped into a unique namespace (derived from \a instanceTag)
34+
/// so that several such macros — which all export identically named symbols
35+
/// (build(), get_builder_hook_unchecked(), ...) — can coexist in the same Cling
36+
/// session without colliding. Returns nullptr on failure.
37+
///
38+
/// \param macroFile path to the geometry macro (shell variables are expanded)
39+
/// \param instanceTag a short tag used to build a unique, human-readable namespace
40+
TGeoVolume* buildCADVolumeFromMacro(const std::string& macroFile, const std::string& instanceTag);
41+
42+
/// Re-register the TGeo media used in the volume tree rooted at \a top into the O2
43+
/// MaterialManager under ownership of \a modulename, rewiring the volumes to the
44+
/// newly created media. This brings the CAD-imported media under O2's media/cut
45+
/// handling (so that e.g. tracking cuts apply consistently).
46+
void remapCADMedia(TGeoVolume* top, const char* modulename);
47+
48+
} // namespace o2::base
49+
50+
#endif

Detectors/Base/include/DetectorsBase/Detector.h

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -530,9 +530,14 @@ class DetImpl : public o2::base::Detector
530530
{
531531
using Hit_t = typename std::remove_pointer<decltype(static_cast<Det*>(this)->Det::getHits(0))>::type;
532532
using Collector_t = tbb::concurrent_unordered_map<int, std::vector<std::vector<std::unique_ptr<Hit_t>>>>;
533-
static Collector_t hitcollector; // note: we can't put this as member because
534-
// decltype type deduction doesn't seem to work for class members; so we use a static member
535-
// and will use some pointer member to communicate this data to other functions
533+
// note: we can't put this as a member because decltype type deduction doesn't seem to work for
534+
// class members; so we use a static and communicate it to other functions via a pointer member.
535+
// The collector must be kept *per detector instance* (keyed by 'this'): for most detectors there
536+
// is a single instance per C++ type, but several external detectors share the same type
537+
// (o2::ext::ExternalDetector) and would otherwise clobber/double-free each other's buffers.
538+
// tbb::concurrent_unordered_map is node-based, so the reference stays valid across insertions.
539+
static tbb::concurrent_unordered_map<void const*, Collector_t> hitcollectors;
540+
auto& hitcollector = hitcollectors[this];
536541
mHitCollectorBufferPtr = (char*)&hitcollector;
537542

538543
int probe = 0;
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
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 "DetectorsBase/CADGeometryUtils.h"
13+
#include "DetectorsBase/MaterialManager.h"
14+
#include <CommonUtils/FileSystemUtils.h>
15+
#include <TGeoVolume.h>
16+
#include <TGeoNode.h>
17+
#include <TGeoMaterial.h>
18+
#include <TGeoMedium.h>
19+
#include <TInterpreter.h>
20+
#include <TROOT.h>
21+
#include <TString.h>
22+
#include <TGlobal.h>
23+
#include <fairlogger/Logger.h>
24+
#include <atomic>
25+
#include <cctype>
26+
#include <filesystem>
27+
#include <fstream>
28+
#include <functional>
29+
#include <string>
30+
#include <unordered_map>
31+
#include <unordered_set>
32+
33+
namespace o2::base
34+
{
35+
36+
TGeoVolume* buildCADVolumeFromMacro(const std::string& macroFile, const std::string& instanceTag)
37+
{
38+
if (macroFile.empty()) {
39+
return nullptr;
40+
}
41+
auto expandedHookFileName = o2::utils::expandShellVarsInFileName(macroFile);
42+
if (!std::filesystem::exists(expandedHookFileName)) {
43+
LOG(error) << "External geometry macro " << expandedHookFileName << " does not exist";
44+
return nullptr;
45+
}
46+
47+
// We JIT the macro into a *unique* namespace per call. This is essential when several
48+
// external geometries are present at the same time: every macro produced by
49+
// O2_CADtoTGeo.py exports identically named symbols (build(), get_builder_hook_unchecked(),
50+
// LoadFacets(), ...). Loading them all into the single global Cling scope would collide
51+
// (the first definition wins and subsequent macros are silently ignored). By wrapping each
52+
// macro body in its own namespace we keep the symbols separate. The preprocessor #include
53+
// lines must stay at global scope, so we hoist them out of the namespace.
54+
std::ifstream macroStream(expandedHookFileName, std::ios::in);
55+
if (!macroStream.is_open()) {
56+
LOG(error) << "Cannot open external geometry macro " << expandedHookFileName;
57+
return nullptr;
58+
}
59+
std::string preamble; // #include (and other top-level preprocessor) lines -> global scope
60+
std::string body; // everything else -> wrapped into a unique namespace
61+
std::string line;
62+
while (std::getline(macroStream, line)) {
63+
auto firstNonSpace = line.find_first_not_of(" \t");
64+
if (firstNonSpace != std::string::npos && line[firstNonSpace] == '#') {
65+
preamble += line + "\n";
66+
} else {
67+
body += line + "\n";
68+
}
69+
}
70+
71+
// build a unique, valid C++ identifier for the namespace
72+
static std::atomic<int> instanceCounter{0};
73+
std::string ns = std::string("o2_cadgeom_") + instanceTag + "_" + std::to_string(instanceCounter++);
74+
for (auto& c : ns) {
75+
if (!std::isalnum(static_cast<unsigned char>(c)) && c != '_') {
76+
c = '_';
77+
}
78+
}
79+
80+
const std::string wrapped = preamble + "\nnamespace " + ns + " {\n" + body + "\n}\n";
81+
if (!gInterpreter->Declare(wrapped.c_str())) {
82+
LOG(error) << "Failed to JIT external geometry macro " << expandedHookFileName;
83+
return nullptr;
84+
}
85+
86+
// retrieve the builder hook from the unique namespace
87+
const std::string globalName = "__" + ns + "_hook__";
88+
gROOT->ProcessLine(Form("std::function<TGeoVolume*()> %s = %s::get_builder_hook_unchecked();",
89+
globalName.c_str(), ns.c_str()));
90+
auto global = gROOT->GetGlobal(globalName.c_str());
91+
if (!global) {
92+
LOG(error) << "Could not retrieve geometry builder hook from macro " << expandedHookFileName;
93+
return nullptr;
94+
}
95+
auto hook = *reinterpret_cast<std::function<TGeoVolume*()>*>(global->GetAddress());
96+
LOG(info) << "CAD geometry hook initialized from file " << expandedHookFileName << " (namespace " << ns << ")";
97+
98+
auto top = hook();
99+
if (!top) {
100+
LOG(error) << "CAD geometry macro " << expandedHookFileName << " did not return a top volume";
101+
}
102+
return top;
103+
}
104+
105+
void remapCADMedia(TGeoVolume* top, const char* modulename)
106+
{
107+
std::unordered_map<TGeoMedium*, TGeoMedium*> medium_ptr_mapping;
108+
std::unordered_set<TGeoVolume*> volumes_already_treated;
109+
int counter = 1;
110+
111+
// The transformer function
112+
auto transform_media = [&](TGeoVolume* vol_) {
113+
if (volumes_already_treated.find(vol_) != volumes_already_treated.end()) {
114+
// this volume was already transformed
115+
return;
116+
}
117+
volumes_already_treated.insert(vol_);
118+
119+
if (dynamic_cast<TGeoVolumeAssembly*>(vol_)) {
120+
// do nothing for assemblies (they don't have a medium)
121+
return;
122+
}
123+
124+
auto medium = vol_->GetMedium();
125+
if (!medium) {
126+
return;
127+
}
128+
129+
auto iter = medium_ptr_mapping.find(medium);
130+
if (iter != medium_ptr_mapping.end()) {
131+
// This medium has already been transformed, so
132+
// we just update the volume
133+
vol_->SetMedium(iter->second);
134+
return;
135+
} else {
136+
LOG(info) << "Transforming media with name " << medium->GetName() << " for volume " << vol_->GetName();
137+
138+
// we found a medium, not yet treated
139+
auto curr_mat = medium->GetMaterial();
140+
auto& matmgr = o2::base::MaterialManager::Instance();
141+
142+
matmgr.Material(modulename, counter, curr_mat->GetName(), curr_mat->GetA(), curr_mat->GetZ(), curr_mat->GetDensity(), curr_mat->GetRadLen(), curr_mat->GetIntLen());
143+
// TGeo medium params are stored in a flat array with the following convention
144+
// fParams[0] = isvol;
145+
// fParams[1] = ifield;
146+
// fParams[2] = fieldm;
147+
// fParams[3] = tmaxfd;
148+
// fParams[4] = stemax;
149+
// fParams[5] = deemax;
150+
// fParams[6] = epsil;
151+
// fParams[7] = stmin;
152+
const auto isvol = medium->GetParam(0);
153+
const auto isxfld = medium->GetParam(1);
154+
const auto sxmgmx = medium->GetParam(2);
155+
const auto tmaxfd = medium->GetParam(3);
156+
const auto stemax = medium->GetParam(4);
157+
const auto deemax = medium->GetParam(5);
158+
const auto epsil = medium->GetParam(6);
159+
const auto stmin = medium->GetParam(7);
160+
161+
matmgr.Medium(modulename, counter, medium->GetName(), counter, isvol, isxfld, sxmgmx, tmaxfd, stemax, deemax, epsil, stmin);
162+
163+
// there will be new Material and Medium objects; fetch them
164+
auto new_med = matmgr.getTGeoMedium(modulename, counter);
165+
166+
// insert into cache
167+
medium_ptr_mapping[medium] = new_med;
168+
vol_->SetMedium(new_med);
169+
counter++;
170+
}
171+
}; // end transformer lambda
172+
173+
// a generic volume walker
174+
std::function<void(TGeoVolume*)> visit_volume;
175+
visit_volume = [&](TGeoVolume* vol) -> void {
176+
if (!vol) {
177+
return;
178+
}
179+
180+
// call the transformer
181+
transform_media(vol);
182+
183+
// Recurse into daughters
184+
const int nd = vol->GetNdaughters();
185+
for (int i = 0; i < nd; ++i) {
186+
TGeoNode* node = vol->GetNode(i);
187+
if (!node) {
188+
continue;
189+
}
190+
TGeoVolume* child = node->GetVolume();
191+
if (!child) {
192+
continue;
193+
}
194+
195+
visit_volume(child);
196+
}
197+
};
198+
199+
visit_volume(top);
200+
}
201+
202+
} // namespace o2::base

Detectors/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ add_subdirectory(TOF)
2525
add_subdirectory(ZDC)
2626

2727
add_subdirectory(ITSMFT)
28+
add_subdirectory(External) # sensitive external (CAD-derived) detectors; uses ITSMFT hit type
2829
add_subdirectory(TRD)
2930

3031
add_subdirectory(MUON)

Detectors/External/CMakeLists.txt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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+
o2_add_library(ExternalDetectors
13+
SOURCES src/ExternalDetector.cxx
14+
PUBLIC_LINK_LIBRARIES O2::DetectorsBase
15+
O2::SimulationDataFormat
16+
O2::CommonUtils
17+
RapidJSON::RapidJSON)
18+
19+
o2_target_root_dictionary(ExternalDetectors
20+
HEADERS include/ExternalDetectors/Hit.h
21+
include/ExternalDetectors/ExternalDetector.h
22+
LINKDEF src/ExternalDetectorsLinkDef.h)

0 commit comments

Comments
 (0)