Skip to content

Commit 2d08038

Browse files
sawenzelclaude
andcommitted
External: add SimExample with two artificial sensitive detectors
A tiny, self-contained run/SimExamples entry demonstrating data-driven injection of sensitive external detectors into o2-sim (no code compiled in): - ACYL : silicon barrel cylinder, DetID slot ITS, built-in action - BDISK: silicon endcap disk, DetID slot TST, custom JITed action Both are instances of o2::ext::ExternalDetector producing the generic o2::ext::Hit. Geometry is built at runtime from hand-written .macro stand-ins for O2_CADtoTGeo.py output, so the example needs no CAD binaries. run.sh transports a few boxgen events and inspect_hits.macro prints the per-detector hit counts, exercising the parallel-mode hit-merger persistence path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 374a336 commit 2d08038

8 files changed

Lines changed: 279 additions & 0 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# External sensitive detectors
2+
3+
A minimal example showing how to add **sensitive** detectors to `o2-sim` without compiling
4+
anything into O2. Two artificial detectors are injected purely from data:
5+
6+
| name | geometry (runtime macro) | DetID slot | sensitive action |
7+
|---------|-------------------------------|------------|---------------------------------|
8+
| `ACYL` | `geometry_innerCylinder.macro`| `ITS` | built-in entrance/exit action |
9+
| `BDISK` | `geometry_outerDisk.macro` | `TST` | custom action `sensitive_action.macro` |
10+
11+
Both are instances of the single compiled `o2::ext::ExternalDetector` class and produce the
12+
single generic hit type `o2::ext::Hit`. They differ only in their data: geometry, the DetID
13+
slot they occupy, and (optionally) a sensitive-action macro.
14+
15+
## Run
16+
17+
```bash
18+
./run.sh
19+
```
20+
21+
This transports a few `boxgen` events and prints the per-detector hit counts, e.g.
22+
23+
```
24+
External sensitive detector hits:
25+
ACYLHit : ~430 hits over 5 events, mean radius 20.0 cm [o2sim_HitsITS.root]
26+
BDISKHit : ~120 hits over 5 events, mean radius ... cm [o2sim_HitsTST.root]
27+
```
28+
29+
## How it works
30+
31+
* **Geometry** — each `"macro"` is a ROOT macro exporting `get_builder_hook_unchecked()`, the
32+
same symbol `O2_CADtoTGeo.py` emits when converting CAD (STEP) files to TGeo. The macros here
33+
are hand-written so the example is self-contained (no CAD binaries). The volumes whose names
34+
match `"sensitiveVolumes"` are registered as sensitive.
35+
36+
* **Sensitive action**`ACYL` has none, so it uses the built-in action that records an
37+
entrance/exit hit per track. `BDISK` points `"sensitiveMacro"` at a macro whose
38+
`sensitiveAction()` returns the per-step callable; it is JIT-compiled at runtime via
39+
`o2::conf::GetFromMacro` (the same mechanism as the generator / stepping hooks — no
40+
recompilation, no ACLIC). The callable has the full `TVirtualMC` singleton and a few helpers
41+
(`currentSensorID()`, `currentTrackID()`, `addHit()`).
42+
43+
* **Persistence** — each detector is tied to a free **DetID** slot (`ITS`, `TST` here, chosen
44+
because no real detector of that name is active). The DetID is the scarce resource: it fixes
45+
the hit-file name (`o2sim_Hits<DetID>.root`) and lets the hit merger instantiate a matching
46+
receiver, so hits are written in parallel multi-worker mode just like for any built-in
47+
detector. The branch keeps the detector's own name (`ACYLHit`, `BDISKHit`).
48+
49+
## Adding more detectors
50+
51+
Append entries to `externalDetectors.json` (each on a different free DetID slot) and list their
52+
names in `detectorlist.json`. The mechanism is fully data-driven; nothing needs to be rebuilt.
53+
54+
See also `Detectors/External` and `scripts/geometry/O2_CADtoTGeo.py`.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"EXTEXAMPLE": [
3+
"ACYL",
4+
"BDISK"
5+
]
6+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"externalDetectors": [
3+
{
4+
"name": "ACYL",
5+
"title": "artificial barrel cylinder - built-in entrance/exit action",
6+
"macro": "geometry_innerCylinder.macro",
7+
"anchor": "barrel",
8+
"detID": "ITS",
9+
"sensitiveVolumes": ["ACYL_SENS"],
10+
"placement": { "translation": [0, 0, 0] }
11+
},
12+
{
13+
"name": "BDISK",
14+
"title": "artificial endcap disk - custom JITed sensitive action",
15+
"macro": "geometry_outerDisk.macro",
16+
"anchor": "barrel",
17+
"detID": "TST",
18+
"sensitiveVolumes": ["BDISK_SENS"],
19+
"sensitiveMacro": "sensitive_action.macro",
20+
"placement": { "translation": [0, 0, 0] }
21+
}
22+
]
23+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Geometry builder for the "ACYL" artificial detector: a thin silicon cylindrical shell.
2+
//
3+
// This is a hand-written stand-in for the macros that O2_CADtoTGeo.py produces from CAD
4+
// files. It exports the same builder-hook symbol (get_builder_hook_unchecked) that
5+
// o2::base::buildCADVolumeFromMacro expects, so it is injected exactly like a CAD module
6+
// through the external-geometry JSON. Media are placeholders here; remapCADMedia()
7+
// re-registers them under O2's MaterialManager at construction time.
8+
#include <TGeoManager.h>
9+
#include <TGeoTube.h>
10+
#include <TGeoMaterial.h>
11+
#include <TGeoMedium.h>
12+
#include <TGeoVolume.h>
13+
#include <functional>
14+
#include <stdexcept>
15+
16+
TGeoVolume* build(bool /*check*/ = true)
17+
{
18+
if (!gGeoManager) {
19+
throw std::runtime_error("gGeoManager is null when building ACYL");
20+
}
21+
auto* mat = new TGeoMaterial("SiACYL", 28.0855, 14., 2.33, 9.37, 45.5);
22+
double params[8] = {1., 0., 0., 0., 0., 0., 0., 0.}; // isvol=1, no field, defaults
23+
auto* med = new TGeoMedium("SiACYL", 1, mat, params);
24+
25+
// 2 mm thick shell, R = 20 cm, half-length 40 cm
26+
auto* tube = new TGeoTube("ACYL_SENS", 20.0, 20.2, 40.0);
27+
auto* sens = new TGeoVolume("ACYL_SENS", tube, med);
28+
29+
auto* top = new TGeoVolumeAssembly("ACYL");
30+
top->AddNode(sens, 1, nullptr);
31+
return top;
32+
}
33+
34+
std::function<TGeoVolume*()> get_builder_hook_checked() { return []() { return build(true); }; }
35+
std::function<TGeoVolume*()> get_builder_hook_unchecked() { return []() { return build(false); }; }
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Geometry builder for the "BDISK" artificial detector: a thin silicon endcap disk.
2+
//
3+
// Hand-written stand-in for a CAD-exported macro (see geometry_innerCylinder.macro). Exports the
4+
// same builder-hook symbol consumed by o2::base::buildCADVolumeFromMacro. Together with ACYL
5+
// this gives two geometrically distinct artificial detectors (a barrel shell and an endcap
6+
// disk) sharing one generic o2::ext::Hit type but writing into separate DetID slots.
7+
#include <TGeoManager.h>
8+
#include <TGeoTube.h>
9+
#include <TGeoMaterial.h>
10+
#include <TGeoMedium.h>
11+
#include <TGeoVolume.h>
12+
#include <TGeoMatrix.h>
13+
#include <functional>
14+
#include <stdexcept>
15+
16+
TGeoVolume* build(bool /*check*/ = true)
17+
{
18+
if (!gGeoManager) {
19+
throw std::runtime_error("gGeoManager is null when building BDISK");
20+
}
21+
auto* mat = new TGeoMaterial("SiBDISK", 28.0855, 14., 2.33, 9.37, 45.5);
22+
double params[8] = {1., 0., 0., 0., 0., 0., 0., 0.}; // isvol=1, no field, defaults
23+
auto* med = new TGeoMedium("SiBDISK", 1, mat, params);
24+
25+
// a 2 mm thick disk: inner r = 5 cm, outer r = 40 cm, placed at z = +70 cm (endcap)
26+
auto* disk = new TGeoTube("BDISK_SENS", 5.0, 40.0, 0.1);
27+
auto* sens = new TGeoVolume("BDISK_SENS", disk, med);
28+
29+
auto* top = new TGeoVolumeAssembly("BDISK");
30+
top->AddNode(sens, 1, new TGeoTranslation(0., 0., 70.0));
31+
return top;
32+
}
33+
34+
std::function<TGeoVolume*()> get_builder_hook_checked() { return []() { return build(true); }; }
35+
std::function<TGeoVolume*()> get_builder_hook_unchecked() { return []() { return build(false); }; }
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Inspect the hits produced by the two artificial external detectors of this example.
2+
//
3+
// In parallel mode (o2-sim) the hit merger writes one file per detector, named after the
4+
// DetID slot the detector borrows: ACYL -> o2sim_HitsITS.root, BDISK -> o2sim_HitsTST.root.
5+
// The branch inside keeps the detector's own name: "ACYLHit" / "BDISKHit".
6+
//
7+
// Run: root -l -b -q inspect_hits.macro
8+
#if !defined(__CLING__) || defined(__ROOTCLING__)
9+
#include "ExternalDetectors/Hit.h"
10+
#include <TFile.h>
11+
#include <TTree.h>
12+
#include <vector>
13+
#include <cmath>
14+
#endif
15+
16+
void inspectOne(const char* fname, const char* branch)
17+
{
18+
auto* f = TFile::Open(fname);
19+
if (!f || f->IsZombie()) {
20+
printf(" %-22s : file not found (%s)\n", branch, fname);
21+
return;
22+
}
23+
auto* t = (TTree*)f->Get("o2sim");
24+
if (!t || !t->GetBranch(branch)) {
25+
printf(" %-22s : no '%s' branch in %s\n", branch, branch, fname);
26+
delete f;
27+
return;
28+
}
29+
std::vector<o2::ext::Hit>* hits = nullptr;
30+
t->SetBranchAddress(branch, &hits);
31+
long total = 0;
32+
double rsum = 0.;
33+
for (Long64_t e = 0; e < t->GetEntries(); ++e) {
34+
t->GetEntry(e);
35+
for (auto& h : *hits) {
36+
++total;
37+
rsum += std::hypot(h.GetX(), h.GetY());
38+
}
39+
}
40+
printf(" %-22s : %ld hits over %lld events, mean radius %.1f cm [%s]\n",
41+
branch, total, t->GetEntries(), total ? rsum / total : 0., fname);
42+
delete f;
43+
}
44+
45+
void inspect_hits()
46+
{
47+
printf("External sensitive detector hits:\n");
48+
inspectOne("o2sim_HitsITS.root", "ACYLHit"); // barrel cylinder, built-in action
49+
inspectOne("o2sim_HitsTST.root", "BDISKHit"); // endcap disk, custom JITed action
50+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Minimal example injecting two artificial *sensitive* external detectors into o2-sim.
4+
#
5+
# Neither detector is compiled into O2: both are described purely by data. The geometry of
6+
# each is built at runtime from a ROOT macro (here hand-written stand-ins for what
7+
# O2_CADtoTGeo.py produces from CAD files), and each is tied to a free DetID slot so its hits
8+
# are written like those of any built-in detector -- including in parallel (multi-worker) mode,
9+
# where the hit merger instantiates a matching receiver per detector.
10+
#
11+
# ACYL : thin silicon barrel cylinder, DetID slot ITS, built-in entrance/exit action
12+
# BDISK : thin silicon endcap disk, DetID slot TST, custom JITed sensitive action
13+
#
14+
# Both share the single generic hit type o2::ext::Hit. Multiplicity is data-driven: add more
15+
# entries (on more free DetID slots) to externalDetectors.json and detectorlist.json.
16+
17+
set -x
18+
19+
# run from this example's directory so the relative macro/JSON paths resolve
20+
cd "$(dirname "$0")"
21+
22+
NWORKERS=2
23+
EVENTS=5
24+
25+
o2-sim -j ${NWORKERS} -n ${EVENTS} -g boxgen \
26+
--detectorList EXTEXAMPLE:detectorlist.json \
27+
--extGeomFile externalDetectors.json \
28+
--configKeyValues 'BoxGun.number=50' \
29+
> sim.log 2>&1
30+
31+
# count and locate the produced hits (one file per detector / DetID slot)
32+
root -l -b -q inspect_hits.macro
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Custom sensitive action for the "BDISK" artificial detector.
2+
//
3+
// The action is JIT-compiled at runtime via o2::conf::GetFromMacro (the same mechanism used
4+
// for generator and stepping hooks) and fully replaces ExternalDetector's built-in
5+
// entrance/exit action. It runs for every MC step inside one of the detector's sensitive
6+
// volumes and has the full TVirtualMC singleton available, plus a few convenience helpers on
7+
// the detector (currentSensorID(), currentTrackID(), addHit()).
8+
//
9+
// IMPORTANT: the return type must be spelled exactly as the typedef name passed to
10+
// GetFromMacro ("o2::ext::ExternalDetector::SensitiveFcn"); the loader compares the function's
11+
// return-type name textually, so std::function<...> would not match.
12+
//
13+
// This example records a single point-like hit whenever a charged particle enters a sensitive
14+
// volume.
15+
#if !defined(__CLING__) || defined(__ROOTCLING__)
16+
#include "ExternalDetectors/ExternalDetector.h"
17+
#include "ExternalDetectors/Hit.h"
18+
#include <TVirtualMC.h>
19+
#include <TLorentzVector.h>
20+
#endif
21+
22+
o2::ext::ExternalDetector::SensitiveFcn sensitiveAction()
23+
{
24+
return [](o2::ext::ExternalDetector* det) -> bool {
25+
auto vmc = TVirtualMC::GetMC();
26+
if (vmc->TrackCharge() == 0) {
27+
return false; // ignore neutral particles
28+
}
29+
if (!vmc->IsTrackEntering()) {
30+
return false; // record only the entrance point
31+
}
32+
const int sensor = det->currentSensorID();
33+
if (sensor < 0) {
34+
return false; // not one of our sensitive volumes
35+
}
36+
TLorentzVector pos, mom;
37+
vmc->TrackPosition(pos);
38+
vmc->TrackMomentum(mom);
39+
det->addHit(det->currentTrackID(), sensor, pos.Vect(), pos.Vect(), mom.Vect(),
40+
mom.E(), pos.T(), 0. /*eLoss*/, o2::ext::Hit::kTrackEntering, o2::ext::Hit::kTrackEntering,
41+
vmc->TrackPid(), vmc->TrackLength());
42+
return true;
43+
};
44+
}

0 commit comments

Comments
 (0)