Skip to content

Commit a0dde5b

Browse files
sawenzelclaude
andcommitted
Passive: inject external (CAD) geometry modules from JSON config
Make external geometry modules configurable from the outside instead of hardcoding them in build_geometry.C. Adds: - ExternalModule::createFromJSON(): builds a list of external passive modules from a JSON file ("externalModules" array of {name, macro, anchor, optional placement{translation, rotation_deg}}). - SimConfig: new --extGeomFile option, carried through SimConfigData and exposed via getExtGeomFilename(). - build_geometry.C: replaces the single hardcoded EXT block with a loop that adds every external module whose name is in the active module list. Also fixes a blocker for running several external modules at once: every geom.C produced by O2_CADtoTGeo.py exports identical symbols (build(), get_builder_hook_unchecked(), ...). Loading them all into the single global Cling scope made the first definition win and silently ignored later macros. initGeomBuilderHook() now JITs each macro into a unique per-instance namespace (hoisting #include lines to global scope), so multiple CAD modules coexist. Tested with o2-sim-serial -n 1 injecting two CAD detectors (oTOF and IRIS, the latter with materials from a BOM CSV) simultaneously. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 630c8fb commit a0dde5b

6 files changed

Lines changed: 193 additions & 22 deletions

File tree

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/Passive/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ o2_add_library(DetectorsPassive
2424
src/HallSimParam.cxx
2525
src/PassiveBase.cxx
2626
src/ExternalModule.cxx
27-
PUBLIC_LINK_LIBRARIES O2::Field O2::DetectorsBase O2::SimConfig)
27+
PUBLIC_LINK_LIBRARIES O2::Field O2::DetectorsBase O2::SimConfig
28+
RapidJSON::RapidJSON)
2829

2930
o2_target_root_dictionary(DetectorsPassive
3031
HEADERS include/DetectorsPassive/Absorber.h

Detectors/Passive/include/DetectorsPassive/ExternalModule.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
#include "DetectorsPassive/PassiveBase.h" // base class of passive modules
1616
#include "Rtypes.h" // for Pipe::Class, ClassDef, Pipe::Streamer
17+
#include <string>
18+
#include <vector>
1719

1820
class TGeoVolume;
1921
class TGeoTransformation;
@@ -41,6 +43,13 @@ class ExternalModule : public PassiveBase
4143
~ExternalModule() override = default;
4244
void ConstructGeometry() override;
4345

46+
/// Build a list of external (passive) modules from a JSON description file.
47+
/// The file must contain an "externalModules" array; each entry needs at least
48+
/// "name", "macro" and "anchor"; an optional "placement" object may carry
49+
/// "translation":[x,y,z] (cm) and "rotation_deg":[rx,ry,rz] (degrees).
50+
/// Ownership of the returned modules is transferred to the caller.
51+
static std::vector<ExternalModule*> createFromJSON(const std::string& jsonfile);
52+
4453
/// Clone this object (used in MT mode only)
4554
FairModule* CloneModule() const override { return nullptr; }
4655

Detectors/Passive/src/ExternalModule.cxx

Lines changed: 164 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,25 @@
1414
#include <DetectorsPassive/ExternalModule.h>
1515
#include <CommonUtils/ConfigurationMacroHelper.h>
1616
#include <filesystem>
17+
#include <fstream>
18+
#include <atomic>
19+
#include <cctype>
1720
#include <CommonUtils/FileSystemUtils.h>
1821
#include <TGeoManager.h>
1922
#include <TGeoVolume.h>
23+
#include <TGeoMatrix.h>
24+
#include <TInterpreter.h>
25+
#include <TROOT.h>
26+
#include <TString.h>
27+
#include <TGlobal.h>
2028
#include <unordered_map>
2129
#include <unordered_set>
2230
#include <TGeoMaterial.h>
2331
#include <TGeoMedium.h>
2432
#include <DetectorsBase/MaterialManager.h>
33+
#include <rapidjson/document.h>
34+
#include <rapidjson/error/en.h>
35+
#include <rapidjson/istreamwrapper.h>
2536

2637
// ClassImp(o2::passive::ExternalModule)
2738

@@ -157,19 +168,163 @@ void ExternalModule::ConstructGeometry()
157168
anchor->AddNode(const_cast<TGeoVolume*>(module_top), 1, const_cast<TGeoMatrix*>(mOptions.placement));
158169
}
159170

171+
namespace
172+
{
173+
// Build a TGeoCombiTrans from an optional JSON "placement" object carrying
174+
// "translation":[x,y,z] (cm) and/or "rotation_deg":[rx,ry,rz] (deg, applied X,Y,Z).
175+
TGeoMatrix* makePlacementFromJSON(const rapidjson::Value& placement)
176+
{
177+
auto combi = new TGeoCombiTrans();
178+
if (placement.HasMember("rotation_deg") && placement["rotation_deg"].IsArray()) {
179+
const auto& r = placement["rotation_deg"];
180+
if (r.Size() == 3) {
181+
combi->RotateX(r[0].GetDouble());
182+
combi->RotateY(r[1].GetDouble());
183+
combi->RotateZ(r[2].GetDouble());
184+
} else {
185+
LOG(warning) << "ExternalModule placement 'rotation_deg' must have 3 entries; ignoring";
186+
}
187+
}
188+
if (placement.HasMember("translation") && placement["translation"].IsArray()) {
189+
const auto& t = placement["translation"];
190+
if (t.Size() == 3) {
191+
combi->SetDx(t[0].GetDouble());
192+
combi->SetDy(t[1].GetDouble());
193+
combi->SetDz(t[2].GetDouble());
194+
} else {
195+
LOG(warning) << "ExternalModule placement 'translation' must have 3 entries; ignoring";
196+
}
197+
}
198+
return combi;
199+
}
200+
} // namespace
201+
202+
std::vector<ExternalModule*> ExternalModule::createFromJSON(const std::string& jsonfile)
203+
{
204+
std::vector<ExternalModule*> result;
205+
206+
auto expanded = o2::utils::expandShellVarsInFileName(jsonfile);
207+
std::ifstream fileStream(expanded, std::ios::in);
208+
if (!fileStream.is_open()) {
209+
LOG(error) << "Cannot open external geometry config file '" << expanded << "'";
210+
return result;
211+
}
212+
213+
rapidjson::IStreamWrapper isw(fileStream);
214+
rapidjson::Document doc;
215+
doc.ParseStream(isw);
216+
if (doc.HasParseError()) {
217+
LOG(error) << "Error parsing external geometry JSON '" << expanded << "': "
218+
<< rapidjson::GetParseError_En(doc.GetParseError())
219+
<< " (offset " << doc.GetErrorOffset() << ")";
220+
return result;
221+
}
222+
if (!doc.HasMember("externalModules") || !doc["externalModules"].IsArray()) {
223+
LOG(error) << "External geometry JSON '" << expanded << "' must contain an 'externalModules' array";
224+
return result;
225+
}
226+
227+
auto getString = [](const rapidjson::Value& v, const char* key) -> std::string {
228+
if (v.HasMember(key) && v[key].IsString()) {
229+
return v[key].GetString();
230+
}
231+
return std::string();
232+
};
233+
234+
for (const auto& entry : doc["externalModules"].GetArray()) {
235+
if (!entry.IsObject()) {
236+
LOG(error) << "Skipping non-object entry in 'externalModules'";
237+
continue;
238+
}
239+
const auto name = getString(entry, "name");
240+
if (name.empty()) {
241+
LOG(error) << "Skipping external module entry without 'name'";
242+
continue;
243+
}
244+
ExternalModuleOptions options;
245+
options.root_macro_file = getString(entry, "macro");
246+
options.anchor_volume = getString(entry, "anchor");
247+
if (options.root_macro_file.empty() || options.anchor_volume.empty()) {
248+
LOG(error) << "External module '" << name << "' requires both 'macro' and 'anchor'; skipping";
249+
continue;
250+
}
251+
if (entry.HasMember("placement") && entry["placement"].IsObject()) {
252+
options.placement = makePlacementFromJSON(entry["placement"]);
253+
}
254+
auto title = getString(entry, "title");
255+
if (title.empty()) {
256+
title = name;
257+
}
258+
LOG(info) << "Configured external module '" << name << "' from macro '" << options.root_macro_file
259+
<< "' anchored to volume '" << options.anchor_volume << "'";
260+
result.push_back(new ExternalModule(name.c_str(), title.c_str(), options));
261+
}
262+
return result;
263+
}
264+
160265
bool ExternalModule::initGeomBuilderHook()
161266
{
162-
if (mOptions.root_macro_file.size() > 0) {
163-
LOG(info) << "Initializing the hook for geometry module building";
164-
auto expandedHookFileName = o2::utils::expandShellVarsInFileName(mOptions.root_macro_file);
165-
if (std::filesystem::exists(expandedHookFileName)) {
166-
// if this file exists we will compile the hook on the fly (the last one is an identifier --> maybe make it dependent on this class)
167-
mGeomHook = o2::conf::GetFromMacro<GeomBuilderFcn>(mOptions.root_macro_file, "get_builder_hook_unchecked()", "function<TGeoVolume*()>", "o2_passive_extmodule_builder");
168-
LOG(info) << "Hook initialized from file " << expandedHookFileName;
169-
return true;
267+
if (mOptions.root_macro_file.empty()) {
268+
return false;
269+
}
270+
LOG(info) << "Initializing the hook for geometry module building";
271+
auto expandedHookFileName = o2::utils::expandShellVarsInFileName(mOptions.root_macro_file);
272+
if (!std::filesystem::exists(expandedHookFileName)) {
273+
LOG(error) << "External geometry macro " << expandedHookFileName << " does not exist";
274+
return false;
275+
}
276+
277+
// We JIT the macro into a *unique* namespace per module instance. This is essential
278+
// when several external modules are present at the same time: every macro produced by
279+
// O2_CADtoTGeo.py exports identically named symbols (build(), get_builder_hook_unchecked(),
280+
// LoadFacets(), ...). Loading them all into the single global Cling scope would collide
281+
// (the first definition wins and subsequent macros are silently ignored). By wrapping each
282+
// macro body in its own namespace we keep the symbols separate. The preprocessor #include
283+
// lines must stay at global scope, so we hoist them out of the namespace.
284+
std::ifstream macroStream(expandedHookFileName, std::ios::in);
285+
if (!macroStream.is_open()) {
286+
LOG(error) << "Cannot open external geometry macro " << expandedHookFileName;
287+
return false;
288+
}
289+
std::string preamble; // #include (and other top-level preprocessor) lines -> global scope
290+
std::string body; // everything else -> wrapped into a unique namespace
291+
std::string line;
292+
while (std::getline(macroStream, line)) {
293+
auto firstNonSpace = line.find_first_not_of(" \t");
294+
if (firstNonSpace != std::string::npos && line[firstNonSpace] == '#') {
295+
preamble += line + "\n";
296+
} else {
297+
body += line + "\n";
298+
}
299+
}
300+
301+
// build a unique, valid C++ identifier for the namespace
302+
static std::atomic<int> instanceCounter{0};
303+
std::string ns = std::string("o2_ext_") + GetName() + "_" + std::to_string(instanceCounter++);
304+
for (auto& c : ns) {
305+
if (!std::isalnum(static_cast<unsigned char>(c)) && c != '_') {
306+
c = '_';
170307
}
171308
}
172-
return false;
309+
310+
const std::string wrapped = preamble + "\nnamespace " + ns + " {\n" + body + "\n}\n";
311+
if (!gInterpreter->Declare(wrapped.c_str())) {
312+
LOG(error) << "Failed to JIT external geometry macro " << expandedHookFileName;
313+
return false;
314+
}
315+
316+
// retrieve the builder hook from the unique namespace
317+
const std::string globalName = "__" + ns + "_hook__";
318+
gROOT->ProcessLine(Form("std::function<TGeoVolume*()> %s = %s::get_builder_hook_unchecked();",
319+
globalName.c_str(), ns.c_str()));
320+
auto global = gROOT->GetGlobal(globalName.c_str());
321+
if (!global) {
322+
LOG(error) << "Could not retrieve geometry builder hook from macro " << expandedHookFileName;
323+
return false;
324+
}
325+
mGeomHook = *reinterpret_cast<GeomBuilderFcn*>(global->GetAddress());
326+
LOG(info) << "Hook initialized from file " << expandedHookFileName << " (namespace " << ns << ")";
327+
return true;
173328
}
174329

175330
} // namespace o2::passive

macro/build_geometry.C

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -184,16 +184,18 @@ void build_geometry(FairRunSim* run = nullptr)
184184
}
185185
#endif
186186

187-
if (isActivated("EXT")) {
188-
// EXAMPLE!! how to pick geometry generated from external (CAD) module via `O2_CADtoTGeo.py`
189-
o2::passive::ExternalModuleOptions options;
190-
options.root_macro_file = "PATH_TO_EXTERNAL_GEOM_MODULE/geom.C";
191-
options.anchor_volume = "barrel"; // hook this into barrel
192-
auto rot = new TGeoCombiTrans();
193-
rot->RotateX(90);
194-
rot->SetDy(30); // we need to compensate for a shift of barrel with respect to zero
195-
options.placement = rot;
196-
run->AddModule(new o2::passive::ExternalModule("FOO", "BAR", options));
187+
// external (e.g. CAD-derived) geometry modules are injected from the outside via a JSON
188+
// description file given with `--extGeomFile` (geometry generated via `O2_CADtoTGeo.py`).
189+
// Each module is added when its 'name' is part of the active module list (so it can be
190+
// switched on/off via the detector list, like any other module).
191+
if (auto extGeomFile = confref.getExtGeomFilename(); !extGeomFile.empty()) {
192+
for (auto* extmod : o2::passive::ExternalModule::createFromJSON(extGeomFile)) {
193+
if (isActivated(extmod->GetName())) {
194+
run->AddModule(extmod);
195+
} else {
196+
delete extmod; // not requested in the active module list
197+
}
198+
}
197199
}
198200

199201
// the absorber

0 commit comments

Comments
 (0)