Skip to content

Commit 9e82d1a

Browse files
sawenzelclaude
andcommitted
External: add sensitive ExternalDetector + shared CAD geometry utils
Introduce o2::ext::ExternalDetector, a CRTP o2::base::DetImpl detector that injects a CAD-derived geometry (from the O2_CADtoTGeo ROOT macro), marks configured volumes/media sensitive, and produces o2::itsmft::Hit hits under a borrowed DetID. This inherits the full FairMQ hit forwarding/merging machinery via DetImpl. Demonstrated end-to-end in serial o2-sim: sensitive steps -> hits persisted to o2sim.root. Factor the CAD-macro JIT + media remapping shared with the passive ExternalModule into o2::base::CADGeometryUtils (buildCADVolumeFromMacro / remapCADMedia) and simplify ExternalModule to use it. Configured from JSON ("externalDetectors": geometry macro, anchor volume, placement, sensitive volume/media selectors, detID). Wired into build_geometry.C and the allsim aggregate. Known follow-ups (see scripts/geometry/TODO.md): generic Hit type, per-detector JITed sensitive actions, and registering external-detector instances in the parallel hit-merger so per-detector hit files are written in parallel mode. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent a0dde5b commit 9e82d1a

13 files changed

Lines changed: 880 additions & 194 deletions

File tree

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
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: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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::ITSMFTSimulation
16+
RapidJSON::RapidJSON)
17+
18+
o2_target_root_dictionary(ExternalDetectors
19+
HEADERS include/ExternalDetectors/ExternalDetector.h
20+
LINKDEF src/ExternalDetectorsLinkDef.h)

0 commit comments

Comments
 (0)