Skip to content

Commit 374a336

Browse files
sawenzelclaude
andcommitted
External: generic Hit, JITed sensitive actions + HitMerger persistence
Make external (CAD-derived) sensitive detectors first-class in o2-sim: multiple instances, each on its own DetID, sharing one fixed wire format, with per-detector sensitive behaviour and full parallel-mode persistence. - Generic o2::ext::Hit (BasicXYZEHit-based) storing entrance+exit position, momentum, energy, energy loss, time, length, PDG and start/end status. One wire format for all external detectors so the merger stays type-uniform. - ExternalDetector ProcessHits delegates to an optional user action loaded at runtime from a ROOT macro via GetFromMacro (the same JIT mechanism as the generator/TrackReference hooks; no ACLIC). When none is given the built-in entrance/exit action runs. Macro authors get currentSensorID(), currentTrackID() and addHit() helpers and the bare VMC singleton. New JSON keys: sensitiveMacro, sensitiveFunction. - O2HitMerger: register external detectors so their hits are written in parallel (multi-worker) mode. The merger parses the extGeomFile, places an ExternalDetector instance on each configured (free) DetID and opens the matching per-detector hit file; geometry is not built merger-side. - DetImpl::collectHits kept a single function-local static hit collector. That is fine when there is one instance per C++ type, but several external detectors share the type o2::ext::ExternalDetector and clobbered/double-freed each other's buffer (merger segfault). Key the collector by instance (this). - Example sensitiveActionExample.macro records an entrance hit for charged tracks; its return type is spelled as the SensitiveFcn typedef so the GetFromMacro textual return-type check matches. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 9e82d1a commit 374a336

8 files changed

Lines changed: 396 additions & 42 deletions

File tree

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;

Detectors/External/CMakeLists.txt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@
1212
o2_add_library(ExternalDetectors
1313
SOURCES src/ExternalDetector.cxx
1414
PUBLIC_LINK_LIBRARIES O2::DetectorsBase
15-
O2::ITSMFTSimulation
15+
O2::SimulationDataFormat
16+
O2::CommonUtils
1617
RapidJSON::RapidJSON)
1718

1819
o2_target_root_dictionary(ExternalDetectors
19-
HEADERS include/ExternalDetectors/ExternalDetector.h
20+
HEADERS include/ExternalDetectors/Hit.h
21+
include/ExternalDetectors/ExternalDetector.h
2022
LINKDEF src/ExternalDetectorsLinkDef.h)

Detectors/External/include/ExternalDetectors/ExternalDetector.h

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,32 @@
1414
///
1515
/// ExternalDetector is the sensitive counterpart of o2::passive::ExternalModule.
1616
/// It injects a CAD-derived TGeo geometry (produced by scripts/geometry/O2_CADtoTGeo.py)
17-
/// and turns a configurable set of its volumes (selected by their medium name) into
17+
/// and turns a configurable set of its volumes (selected by medium or volume name) into
1818
/// sensitive volumes which produce hits. It derives from o2::base::DetImpl, so it
1919
/// transparently participates in the full o2-sim hit forwarding/merging machinery
2020
/// (FairMQ serialization, sub-event merging, ...).
2121
///
22-
/// As discussed, the detector identity (hit output format) is tied to an existing
23-
/// o2::detectors::DetID. For this first demonstrator it borrows the ITS hit type
24-
/// (o2::itsmft::Hit) and routes its hits to a configurable DetID (ITS by default).
22+
/// All instances share one generic hit type (o2::ext::Hit) so that an arbitrary number
23+
/// of external detectors can coexist (each tied to a different o2::detectors::DetID)
24+
/// without the hit merger needing to know more than this single wire format.
25+
///
26+
/// The sensitive action itself is configurable: by default a generic entrance/exit hit
27+
/// is produced, but a user can instead provide a ROOT macro (loaded at runtime via
28+
/// o2::conf::GetFromMacro, the same mechanism used for generator/stepping hooks) whose
29+
/// function receives the detector instance and may query the TVirtualMC singleton and
30+
/// call addHit(...) to implement an arbitrary sensitive action -- without recompiling O2.
2531

2632
#ifndef ALICEO2_EXT_EXTERNALDETECTOR_H
2733
#define ALICEO2_EXT_EXTERNALDETECTOR_H
2834

2935
#include "DetectorsBase/Detector.h" // for DetImpl
3036
#include "DetectorsCommonDataFormats/DetID.h" // for DetID
31-
#include "ITSMFTSimulation/Hit.h" // for the (borrowed) hit type
37+
#include "ExternalDetectors/Hit.h" // for the generic external hit type
3238

3339
#include "Rtypes.h"
3440
#include "TLorentzVector.h"
3541

42+
#include <functional>
3643
#include <set>
3744
#include <string>
3845
#include <unordered_map>
@@ -53,11 +60,19 @@ struct ExternalDetectorOptions {
5360
std::vector<std::string> sensitiveMedia; // media (substring match on the medium name) to be made sensitive
5461
std::vector<std::string> sensitiveVolumes; // volumes (substring match on the volume name) to be made sensitive
5562
int detID = o2::detectors::DetID::ITS; // DetID this detector's hits are tied to (identity / output format)
63+
std::string sensitiveMacro; // optional ROOT macro implementing the sensitive action
64+
std::string sensitiveFunction; // global function in the macro returning the action (default "sensitiveAction()")
5665
};
5766

5867
class ExternalDetector : public o2::base::DetImpl<ExternalDetector>
5968
{
6069
public:
70+
/// Signature of a (JIT-able) sensitive action. The function is handed the detector
71+
/// instance and is expected to query the TVirtualMC singleton (TVirtualMC::GetMC())
72+
/// for the current step and to call addHit(...) to produce hits. Returning true means
73+
/// a hit-relevant step was processed (mirrors the ProcessHits return value).
74+
using SensitiveFcn = std::function<bool(o2::ext::ExternalDetector*)>;
75+
6176
ExternalDetector(const char* name, const char* title, ExternalDetectorOptions options);
6277
ExternalDetector();
6378
~ExternalDetector() override;
@@ -84,7 +99,7 @@ class ExternalDetector : public o2::base::DetImpl<ExternalDetector>
8499
void Register() override;
85100

86101
/// Get the produced hit collection (probe interface used by DetImpl).
87-
std::vector<o2::itsmft::Hit>* getHits(Int_t iColl) const
102+
std::vector<o2::ext::Hit>* getHits(Int_t iColl) const
88103
{
89104
if (iColl == 0) {
90105
return mHits;
@@ -100,10 +115,27 @@ class ExternalDetector : public o2::base::DetImpl<ExternalDetector>
100115
void PostTrack() override {}
101116
void PreTrack() override {}
102117

118+
/// \name Helpers usable from a user-provided sensitive-action macro
119+
/// These wrap the bookkeeping a sensitive action typically needs so that a macro can
120+
/// stay focused on physics and the TVirtualMC queries.
121+
///@{
122+
/// Append a hit to the output collection and flag the MCTrack as having left a hit
123+
/// in this detector. Returns a pointer to the stored hit.
124+
o2::ext::Hit* addHit(int trackID, int sensorID, const TVector3& startPos, const TVector3& endPos,
125+
const TVector3& startMom, double startE, double endTime, double eLoss,
126+
unsigned char startStatus, unsigned char endStatus, int pdg = 0, float length = 0.f);
127+
128+
/// Running sensor index of the volume currently being processed, or -1 if the current
129+
/// volume is not one of the configured sensitive volumes.
130+
int currentSensorID() const;
131+
132+
/// MCTrack number of the track currently being stepped.
133+
int currentTrackID() const;
134+
///@}
135+
103136
protected:
104-
o2::itsmft::Hit* addHit(int trackID, int detID, const TVector3& startPos, const TVector3& endPos,
105-
const TVector3& startMom, double startE, double endTime, double eLoss,
106-
unsigned char startStatus, unsigned char endStatus);
137+
/// the built-in sensitive action used when no macro is configured (generic entrance/exit hit)
138+
Bool_t defaultProcessHits();
107139

108140
/// recursively collect names of volumes whose medium matches the configured sensitive media
109141
void collectSensitiveVolumeNames(TGeoVolume* vol, std::set<TGeoVolume*>& visited);
@@ -123,7 +155,10 @@ class ExternalDetector : public o2::base::DetImpl<ExternalDetector>
123155
double mEnergyLoss; //! accumulated energy loss
124156
} mTrackData; //!
125157

126-
std::vector<o2::itsmft::Hit>* mHits = nullptr; //! container for produced hits
158+
std::vector<o2::ext::Hit>* mHits = nullptr; //! container for produced hits
159+
160+
SensitiveFcn mSensitiveAction; //! optional user-provided sensitive action (loaded from a macro)
161+
FairVolume* mCurrentVolume = nullptr; //! volume currently passed to ProcessHits (for the action helpers)
127162

128163
int mStepCount = 0; //! number of stepping calls inside our sensitive volumes this event (probe)
129164

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
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 Hit.h
13+
/// \brief Generic hit type for externally injected (CAD-derived) sensitive detectors
14+
///
15+
/// o2::ext::Hit is a deliberately rich, detector-agnostic hit. A single external
16+
/// hit type lets an arbitrary number of o2::ext::ExternalDetector instances (each
17+
/// tied to a different DetID) share one wire format, so that the o2-sim hit merger
18+
/// only ever has to know how to (de)serialize this one type, independently of how
19+
/// many external detectors are configured or what their sensitive action does.
20+
///
21+
/// It stores entrance and exit position, the momentum, energy and energy loss, the
22+
/// time, the track length in the volume, the PDG code and the MC status flags at
23+
/// entrance/exit, so that most information an external sensitive action might want
24+
/// to keep is available downstream.
25+
26+
#ifndef ALICEO2_EXT_HIT_H
27+
#define ALICEO2_EXT_HIT_H
28+
29+
#include "SimulationDataFormat/BaseHits.h" // for BasicXYZEHit
30+
#include "CommonUtils/ShmAllocator.h"
31+
#include "Rtypes.h"
32+
#include "TVector3.h"
33+
#include <iosfwd>
34+
35+
namespace o2::ext
36+
{
37+
38+
class Hit : public o2::BasicXYZEHit<float, float>
39+
{
40+
public:
41+
enum HitStatus_t {
42+
kTrackEntering = 0x1,
43+
kTrackInside = 0x1 << 1,
44+
kTrackExiting = 0x1 << 2,
45+
kTrackOut = 0x1 << 3,
46+
kTrackStopped = 0x1 << 4,
47+
kTrackAlive = 0x1 << 5
48+
};
49+
50+
Hit() = default;
51+
52+
/// \param trackID index of the MCTrack
53+
/// \param sensorID index of the sensitive volume (per-detector running id)
54+
/// \param startPos coordinates at entrance to the active volume [cm]
55+
/// \param endPos coordinates at exit of the active volume [cm]
56+
/// \param startMom momentum of the track at entrance [GeV]
57+
/// \param startE total energy at entrance [GeV]
58+
/// \param endTime time at exit [ns]
59+
/// \param eLoss energy deposited in the volume [GeV]
60+
/// \param startStatus MC status flags at entrance
61+
/// \param endStatus MC status flags at exit
62+
/// \param pdg PDG code of the track (optional)
63+
/// \param length track length inside the volume [cm] (optional)
64+
Hit(int trackID, unsigned short sensorID, const TVector3& startPos, const TVector3& endPos,
65+
const TVector3& startMom, double startE, double endTime, double eLoss,
66+
unsigned char startStatus, unsigned char endStatus, int pdg = 0, float length = 0.f)
67+
: BasicXYZEHit(endPos.X(), endPos.Y(), endPos.Z(), endTime, eLoss, trackID, sensorID),
68+
mMomentum(startMom.Px(), startMom.Py(), startMom.Pz()),
69+
mPosStart(startPos.X(), startPos.Y(), startPos.Z()),
70+
mE(startE),
71+
mLength(length),
72+
mPdg(pdg),
73+
mTrackStatusEnd(endStatus),
74+
mTrackStatusStart(startStatus)
75+
{
76+
}
77+
78+
// entrance position
79+
math_utils::Point3D<float> GetPosStart() const { return mPosStart; }
80+
float GetStartX() const { return mPosStart.X(); }
81+
float GetStartY() const { return mPosStart.Y(); }
82+
float GetStartZ() const { return mPosStart.Z(); }
83+
void SetPosStart(const math_utils::Point3D<float>& p) { mPosStart = p; }
84+
85+
// momentum / energy
86+
math_utils::Vector3D<float> GetMomentum() const { return mMomentum; }
87+
math_utils::Vector3D<float>& GetMomentum() { return mMomentum; }
88+
float GetPx() const { return mMomentum.X(); }
89+
float GetPy() const { return mMomentum.Y(); }
90+
float GetPz() const { return mMomentum.Z(); }
91+
float GetE() const { return mE; }
92+
float GetTotalEnergy() const { return mE; }
93+
94+
// extra bookkeeping
95+
float GetLength() const { return mLength; }
96+
void SetLength(float l) { mLength = l; }
97+
int GetPdg() const { return mPdg; }
98+
void SetPdg(int pdg) { mPdg = pdg; }
99+
100+
// status flags
101+
unsigned char GetStatusStart() const { return mTrackStatusStart; }
102+
unsigned char GetStatusEnd() const { return mTrackStatusEnd; }
103+
bool IsEntering() const { return mTrackStatusEnd & kTrackEntering; }
104+
bool IsInside() const { return mTrackStatusEnd & kTrackInside; }
105+
bool IsExiting() const { return mTrackStatusEnd & kTrackExiting; }
106+
bool IsOut() const { return mTrackStatusEnd & kTrackOut; }
107+
bool IsStopped() const { return mTrackStatusEnd & kTrackStopped; }
108+
bool IsAlive() const { return mTrackStatusEnd & kTrackAlive; }
109+
110+
friend std::ostream& operator<<(std::ostream& of, const Hit& point)
111+
{
112+
of << "-I- o2::ext::Hit for track " << point.GetTrackID() << " in sensor " << point.GetDetectorID();
113+
return of;
114+
}
115+
116+
private:
117+
math_utils::Vector3D<float> mMomentum; ///< momentum at entrance
118+
math_utils::Point3D<float> mPosStart; ///< position at entrance (base mPos holds the exit position)
119+
float mE; ///< total energy at entrance
120+
float mLength; ///< track length inside the volume
121+
int mPdg; ///< PDG code of the track
122+
unsigned char mTrackStatusEnd; ///< MC status flag at exit
123+
unsigned char mTrackStatusStart; ///< MC status flag at entrance
124+
125+
ClassDefNV(Hit, 1);
126+
};
127+
128+
} // namespace o2::ext
129+
130+
#ifdef USESHM
131+
namespace std
132+
{
133+
template <>
134+
class allocator<o2::ext::Hit> : public o2::utils::ShmAllocator<o2::ext::Hit>
135+
{
136+
};
137+
} // namespace std
138+
#endif
139+
140+
#endif // ALICEO2_EXT_HIT_H
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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 sensitiveActionExample.macro
13+
/// \brief Example sensitive action for an o2::ext::ExternalDetector
14+
///
15+
/// Point this macro at an "externalDetectors" entry via the "sensitiveMacro" key
16+
/// (an absolute path, or one using shell variables such as $O2_ROOT, is resolved at runtime):
17+
/// "sensitiveMacro": ".../Detectors/External/macro/sensitiveActionExample.macro"
18+
///
19+
/// The global function sensitiveAction() returns the callable that o2-sim invokes for
20+
/// every tracking step inside one of the detector's sensitive volumes. It is loaded at
21+
/// runtime with o2::conf::GetFromMacro (the same just-in-time mechanism used for
22+
/// generator and stepping hooks), so the sensitive logic can be changed without
23+
/// recompiling O2.
24+
///
25+
/// Inside the action you have the full TVirtualMC singleton available (exactly like a
26+
/// hand-written ProcessHits) plus a few convenience helpers on the detector:
27+
/// - det->currentSensorID() : running index of the current sensitive volume (-1 if none)
28+
/// - det->currentTrackID() : MCTrack number of the track being stepped
29+
/// - det->addHit(...) : append an o2::ext::Hit and flag the MCTrack
30+
///
31+
/// This trivial example records one hit each time a charged particle enters a sensitive
32+
/// volume.
33+
34+
#if !defined(__CLING__) || defined(__ROOTCLING__)
35+
#include "ExternalDetectors/ExternalDetector.h"
36+
#include "ExternalDetectors/Hit.h"
37+
#include <TVirtualMC.h>
38+
#include <TLorentzVector.h>
39+
#include <functional>
40+
#endif
41+
42+
// NOTE: the return type must be spelled exactly as the typedef name passed to
43+
// GetFromMacro ("o2::ext::ExternalDetector::SensitiveFcn") -- the loader compares the
44+
// function's return-type name textually, so std::function<...> would not match.
45+
o2::ext::ExternalDetector::SensitiveFcn sensitiveAction()
46+
{
47+
return [](o2::ext::ExternalDetector* det) -> bool {
48+
auto vmc = TVirtualMC::GetMC();
49+
if (vmc->TrackCharge() == 0) {
50+
return false; // ignore neutral particles
51+
}
52+
if (!vmc->IsTrackEntering()) {
53+
return false; // record only the entrance point in this example
54+
}
55+
const int sensor = det->currentSensorID();
56+
if (sensor < 0) {
57+
return false; // current volume is not one of our sensitive volumes
58+
}
59+
TLorentzVector pos, mom;
60+
vmc->TrackPosition(pos);
61+
vmc->TrackMomentum(mom);
62+
// a point-like hit at the entrance (start == end position, no accumulated energy loss)
63+
det->addHit(det->currentTrackID(), sensor, pos.Vect(), pos.Vect(), mom.Vect(),
64+
mom.E(), pos.T(), 0. /*eLoss*/, o2::ext::Hit::kTrackEntering, o2::ext::Hit::kTrackEntering,
65+
vmc->TrackPid(), vmc->TrackLength());
66+
return true;
67+
};
68+
}

0 commit comments

Comments
 (0)