actions
Loading...
Searching...
No Matches
gEventAction.cc
Go to the documentation of this file.
1#include "gEventAction.h"
3
4// geant4
5#include "G4Event.hh"
6#include "G4Threading.hh"
7
8// c++
9#include <string>
10
11// gemc
15
16// c++
17#include <algorithm>
18#include <cctype>
19#include <chrono>
20#include <sstream>
21#include <unordered_set>
22
23namespace {
24GGeneratedParticleBank make_generated_particle_bank(const GParticleRecordEvent& particles) {
26 bank.reserve(particles.size());
27
28 for (const auto& particle : particles) {
29 bank.push_back({
30 particle.name,
31 particle.pid,
32 particle.type,
33 particle.multiplicity,
34 particle.p,
35 particle.theta,
36 particle.phi,
37 particle.vx,
38 particle.vy,
39 particle.vz
40 });
41 }
42
43 return bank;
44}
45
46GAncestorBank make_ancestor_bank(const std::vector<GTrackRecord>& records) {
47 GAncestorBank bank;
48 bank.reserve(records.size());
49 for (const auto& record : records) {
50 bank.push_back({
51 record.pid,
52 record.tid,
53 record.mtid,
54 record.kinetic_energy,
55 record.momentum.x(),
56 record.momentum.y(),
57 record.momentum.z(),
58 record.vertex.x(),
59 record.vertex.y(),
60 record.vertex.z()
61 });
62 }
63 return bank;
64}
65
66bool scalar_bool_option_enabled(const std::shared_ptr<GOptions>& goptions, const std::string& name) {
67 std::string value = goptions->getOptionalScalarString(name).value_or("");
68 std::transform(value.begin(), value.end(), value.begin(),
69 [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
70 return value == "true" || value == "1" || value == "yes" || value == "on";
71}
72
73// Match a detector against an option containing comma- or whitespace-separated names.
74bool detector_is_listed(const std::shared_ptr<GOptions>& goptions, const std::string& option,
75 const std::string& detector) {
76 std::string detectors = goptions->getOptionalScalarString(option).value_or("");
77 std::replace(detectors.begin(), detectors.end(), ',', ' ');
78
79 std::istringstream names(detectors);
80 std::string name;
81 while (names >> name) {
82 if (name == "all" || name == detector) return true;
83 }
84 return false;
85}
86
87// Convert a substring to a non-negative integer, returning false on any malformed input.
88bool to_non_negative_int(const std::string& text, int& out) {
89 if (text.empty()) return false;
90 size_t pos = 0;
91 int value;
92 try { value = std::stoi(text, &pos); }
93 catch (...) { return false; }
94 if (pos != text.size() || value < 0) return false;
95 out = value;
96 return true;
97}
98}
99
100
101// Construct the event action and keep access to shared configuration plus the
102// non-owning thread-local run action used during event finalization.
103GEventAction::GEventAction(const std::shared_ptr<GOptions>& gopt, GRunAction* run_a,
104 std::shared_ptr<GTrackProvenance> provenance) :
106 goptions(gopt),
107 run_action(run_a),
108 track_provenance(std::move(provenance)) {
109 const auto thread_id = G4Threading::G4GetThreadId();
110 const auto desc = "GEventAction " + std::to_string(thread_id);
111 log->debug(CONSTRUCTOR, FUNCTION_NAME, desc);
112 save_all_ancestors = goptions->getSwitch(SAVE_ALL_ANCESTORS_SWITCH);
113 save_original_track = goptions->getSwitch(SAVE_ORIGINAL_TRACK_SWITCH) || save_all_ancestors;
114
115 // Parse the log_every option of the form N or N-NTH. Anything malformed disables the
116 // feature and is reported once (from thread 0) to avoid duplicated warnings across workers.
117 const auto spec_option = goptions->getOptionalScalarString(LOG_EVERY_OPTION);
118 if (spec_option && !spec_option->empty()) {
119 const std::string& spec = *spec_option;
120 const auto dash = spec.find('-');
121 std::string n_part = dash == std::string::npos ? spec : spec.substr(0, dash);
122 std::string nth_part = dash == std::string::npos ? std::string() : spec.substr(dash + 1);
123
124 // Effective worker-thread count, mirroring gemc::get_nthreads clamping (0 means all cores).
125 int nthreads = goptions->getRequiredScalarInt("nthreads");
126 const int ncores = G4Threading::G4GetNumberOfCores();
127 if (nthreads == 0 || nthreads > ncores) nthreads = ncores;
128
129 int n = 0;
130 if (!to_non_negative_int(n_part, n) || n == 0) {
131 if (thread_id <= 0)
132 log->warning("Ignoring invalid -", LOG_EVERY_OPTION, "=", spec,
133 " : N must be a positive integer.");
134 }
135 else if (dash != std::string::npos) {
136 int nth = 0;
137 if (!to_non_negative_int(nth_part, nth) || nth >= nthreads) {
138 if (thread_id <= 0)
139 log->warning("Ignoring invalid -", LOG_EVERY_OPTION, "=", spec,
140 " : thread id must be in [0, ", nthreads - 1, "].");
141 }
142 else {
143 log_every_n = n;
144 log_every_thread = nth;
145 }
146 }
147 else { log_every_n = n; }
148 }
149}
150
151// Print the periodic "Starting event" line, honoring the log module and optional thread filter.
152// The reported event number, count and rate are all per worker thread: each enabled thread logs
153// every N events it processes, showing its own 1-based event count and average rate (events / second).
154void GEventAction::log_event_start(int thread_id) {
155 if (log_every_n <= 0) return;
156 if (log_every_thread >= 0 && log_every_thread != thread_id) return;
157
158 // Anchor this thread's clock on its first counted event, then count this event.
159 const auto now = std::chrono::steady_clock::now();
160 if (log_events_seen == 0) { log_start_time = now; }
161 ++log_events_seen;
162
163 if (log_events_seen % log_every_n != 0) return;
164
165 const double elapsed_s = std::chrono::duration<double>(now - log_start_time).count();
166 const double rate = elapsed_s > 0.0 ? static_cast<double>(log_events_seen) / elapsed_s : 0.0;
167
168 // log_events_seen is this thread's own 1-based count, not the global Geant4 event id.
169 log->info(0, "Starting event n. ", log_events_seen, " in thread ", thread_id,
170 ". Average rate: ", rate, " events / second");
171}
172
173// Begin-of-event hook used mainly for tracing event and thread identifiers.
174void GEventAction::BeginOfEventAction([[maybe_unused]] const G4Event* event) {
175 const auto thread_id = G4Threading::G4GetThreadId();
176 const auto event_id = event->GetEventID();
177
178 log->debug(NORMAL, FUNCTION_NAME, " event id ", event_id, " in thread ", thread_id);
179 if (track_provenance != nullptr) { track_provenance->clear(); }
180
181 log_event_start(thread_id);
182}
183
184// Finalize the event by reading hit collections, digitizing them, routing the
185// resulting payload according to collection mode, and publishing event-mode output.
186void GEventAction::EndOfEventAction([[maybe_unused]] const G4Event* event) {
187 if (run_action == nullptr) {
189 " run_action is null - cannot access digitization routines or streamers.");
190 return;
191 }
192
193 // Count each processed event once, even when it produces no payload.
194 run_action->increment_run_events_processed();
195
196 const auto thread_id = G4Threading::G4GetThreadId();
197 const auto event_id = event->GetEventID();
198
199 auto gevent_header = std::make_unique<GEventHeader>(goptions, event_id, thread_id);
200 auto eventDataCollection = std::make_shared<GEventDataCollection>(goptions, std::move(gevent_header));
201 eventDataCollection->setGeneratedParticles(
202 make_generated_particle_bank(GPrimaryGeneratorAction::currentGeneratedParticleRecords()));
203 eventDataCollection->setGeneratedTrackedParticles(
205
206 auto* const hcs_this_event = event->GetHCofThisEvent();
207 if (hcs_this_event == nullptr) {
208 if (!eventDataCollection->getGeneratedParticles().empty() ||
209 !eventDataCollection->getGeneratedTrackedParticles().empty()) {
210 publish_event_data(eventDataCollection);
211 }
212 return;
213 }
214
215 const auto digi_map = run_action->get_digitization_routines_map();
216 if (digi_map == nullptr) {
218 " no digitization routines map available in thread ", thread_id);
219 return;
220 }
221
222 bool has_event_mode_payload = false;
223 bool has_run_mode_payload = false;
224 const bool also_reject_true_info = scalar_bool_option_enabled(goptions, "also_reject_true_info");
225 std::unordered_set<int> ancestor_track_ids;
226
227 // Loop over every hit collection produced during this event and dispatch each
228 // collection to the digitization routine registered under its collection name.
229 for (G4int hci = 0; hci < hcs_this_event->GetNumberOfCollections(); ++hci) {
230 auto* const this_ghc = static_cast<GHitsCollection*>(hcs_this_event->GetHC(hci));
231 if (this_ghc == nullptr) {
232 continue;
233 }
234
235 const std::string hcSDName = this_ghc->GetSDname();
236 const bool no_digitized = detector_is_listed(goptions, NO_DIGITIZED_OPTION, hcSDName);
237 const bool no_true_info = detector_is_listed(goptions, NO_TRUE_INFO_OPTION, hcSDName);
238
239 log->info(2, FUNCTION_NAME, " worker ", thread_id,
240 " for event number ", event_id,
241 " for collection number ", hci + 1,
242 " collection name: ", hcSDName);
243
244 // Resolve the digitization routine responsible for this collection.
245 const auto it = digi_map->find(hcSDName);
246 if (it == digi_map->end()) {
248 " no digitization routine registered for collection ", hcSDName,
249 " in thread ", thread_id);
250 continue;
251 }
252
253 const auto& digitization_routine = it->second;
254 if (digitization_routine == nullptr) {
256 " digitization routine is null for collection ", hcSDName,
257 " in thread ", thread_id);
258 continue;
259 }
260
261 const auto collection_mode = digitization_routine->collection_mode();
262 size_t accepted_hit_index = 0;
263
264 // Process all hits in the collection. Event-mode digitizers append to the
265 // event container, while run-mode digitizers append to the run container.
266 for (size_t hitIndex = 0; hitIndex < this_ghc->GetSize(); ++hitIndex) {
267
268 auto* const this_hit = static_cast<GHit*>(this_ghc->GetHit(hitIndex));
269 if (this_hit == nullptr) {
270 continue;
271 }
272 if (save_all_ancestors) {
273 const auto track_ids = this_hit->getTids();
274 ancestor_track_ids.insert(track_ids.begin(), track_ids.end());
275 }
276
277 auto digi_data = no_digitized ? nullptr : digitization_routine->digitizeHit(this_hit, hitIndex);
278 bool hit_accepted = digi_data != nullptr;
279
280 // Apply post-digitization threshold and efficiency policies. Plugins may declare a
281 // policy intrinsic or leave it controlled by -applyThresholds / -applyInefficiencies.
282 // Both are evaluated so the detector's random-number sequence remains stable.
283 if (hit_accepted) {
284 const bool skip_threshold = digitization_routine->apply_thresholds(this_hit, digi_data.get());
285 const bool skip_efficiency = digitization_routine->apply_efficiency(this_hit, digi_data.get());
286 if (skip_threshold || skip_efficiency) {
287 digi_data.reset();
288 hit_accepted = false;
289 }
290 }
291
292 if (collection_mode == CollectionMode::event) {
293 if (hit_accepted) {
294 ++accepted_hit_index;
295 digi_data->includeVariable("hitn", static_cast<int>(accepted_hit_index));
296 run_action->record_analysis_digitized(hcSDName, *digi_data);
297 eventDataCollection->addDetectorDigitizedData(hcSDName, std::move(digi_data));
298 has_event_mode_payload = true;
299 }
300 }
301 else if (collection_mode == CollectionMode::run) {
302 if (hit_accepted) {
303 run_action->record_analysis_digitized(hcSDName, *digi_data);
304 run_action->collect_event_data_collections(
305 hcSDName,
306 std::move(digi_data));
307 has_run_mode_payload = true;
308 }
309 }
310
311 // Event output already requires true information. In GUI analysis mode, request it for
312 // run-mode plugins too so their runtime-defined variables can be discovered without an API schema.
313 const bool collect_true = !no_true_info &&
314 (no_digitized || hit_accepted || !also_reject_true_info) &&
315 (collection_mode == CollectionMode::event || run_action->analysis_enabled());
316 if (collect_true) {
317 const size_t output_hit_index = collection_mode == CollectionMode::event && hit_accepted
318 ? accepted_hit_index : hitIndex + 1;
319 auto true_data = digitization_routine->collectTrueInformation(this_hit, output_hit_index);
320 if (save_original_track && track_provenance != nullptr && true_data != nullptr) {
321 const int tid = this_hit->getTid();
322 const G4ThreeVector op = track_provenance->originalTrackMomentum(tid);
323 true_data->includeVariable("otid", track_provenance->originalTrackId(tid));
324 true_data->includeVariable("opid", track_provenance->originalTrackPid(tid));
325 true_data->includeVariable("opx", op.getX());
326 true_data->includeVariable("opy", op.getY());
327 true_data->includeVariable("opz", op.getZ());
328 }
329 if (true_data != nullptr) { run_action->record_analysis_true(hcSDName, *true_data); }
330 if (collection_mode == CollectionMode::event) {
331 eventDataCollection->addDetectorTrueInfoData(hcSDName, std::move(true_data));
332 has_event_mode_payload = true;
333 }
334 }
335 }
336 }
337
338 if (save_all_ancestors && track_provenance != nullptr) {
339 eventDataCollection->setAncestors(
340 make_ancestor_bank(track_provenance->ancestorsForTracks(ancestor_track_ids)));
341 }
342
343 // Record whether this event contributed at least one run-mode payload entry.
344 if (has_run_mode_payload) {
345 run_action->increment_run_events_with_payload();
346 }
347
348 // Publish event-mode output once, after all collections have been processed.
349 if (has_event_mode_payload ||
350 !eventDataCollection->getAncestors().empty() ||
351 !eventDataCollection->getGeneratedParticles().empty() ||
352 !eventDataCollection->getGeneratedTrackedParticles().empty()) {
353 publish_event_data(eventDataCollection);
354 }
355}
356
357// Send the completed event-data object to every configured worker-thread streamer.
358void GEventAction::publish_event_data(const std::shared_ptr<GEventDataCollection>& event_data) const {
359 if (run_action == nullptr || event_data == nullptr) {
360 return;
361 }
362
363 if (!run_action->has_streamer_threads_map()) {
364 return;
365 }
366
367 const auto gstreamers_threads_map = run_action->get_streamer_threads_map();
368 if (gstreamers_threads_map == nullptr) {
369 log->error(gaction::ERR_STREAMERMAP_NOT_EXISTING, FUNCTION_NAME,
370 " no thread streamer map available - event will not be published.");
371 return;
372 }
373
374 for (const auto& [name, gstreamer] : *gstreamers_threads_map) {
375 if (gstreamer == nullptr) {
376 log->error(gaction::ERR_STREAMERMAP_NOT_EXISTING, FUNCTION_NAME,
377 " null gstreamer instance for streamer ", name);
378 continue;
379 }
380
381 gstreamer->publishEventData(event_data);
382 }
383}
GBase(const std::shared_ptr< GOptions > &gopt, std::string logger_name="")
std::shared_ptr< GLogger > log
void EndOfEventAction(const G4Event *event) override
Called by Geant4 at the end of an event.
GEventAction(const std::shared_ptr< GOptions > &gopt, GRunAction *run_a, std::shared_ptr< GTrackProvenance > provenance=nullptr)
Constructs the event action.
void BeginOfEventAction(const G4Event *event) override
Called by Geant4 at the beginning of an event.
static const GParticleRecordEvent & currentGeneratedParticleRecords()
Returns the current event's full generated-particle records.
static const GParticleRecordEvent & currentGeneratedTrackedParticleRecords()
Returns the current event's Geant4-tracked generated-particle records.
Handles run begin/end callbacks and creates the thread-local GRun object.
Definition gRunAction.h:70
Declares GEventAction, the per-event processing action for the GEMC actions module.
constexpr const char * EVENTACTION_LOGGER
constexpr const char * LOG_EVERY_OPTION
Name of the option controlling periodic per-event log messages.
constexpr const char * SAVE_ORIGINAL_TRACK_SWITCH
constexpr const char * NO_DIGITIZED_OPTION
constexpr const char * NO_TRUE_INFO_OPTION
constexpr const char * SAVE_ALL_ANCESTORS_SWITCH
std::vector< GAncestorData > GAncestorBank
std::vector< GGeneratedParticleData > GGeneratedParticleBank
Declares GPrimaryGeneratorAction, the primary-particle generation action for the GEMC actions module.
Defines error codes used by the GEMC actions module.
event
G4THitsCollection< GHit > GHitsCollection
#define FUNCTION_NAME
CONSTRUCTOR
NORMAL
std::vector< GParticleRecord > GParticleRecordEvent
constexpr int ERR_GDIGIMAP_NOT_EXISTING
constexpr int ERR_STREAMERMAP_NOT_EXISTING
constexpr int ERR_GRUNACTION_NOT_EXISTING