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#include "G4RunManager.hh"
8
9// c++
10#include <string>
11
12// gemc
16
17// c++
18#include <algorithm>
19#include <cctype>
20#include <chrono>
21#include <sstream>
22#include <limits>
23#include <unordered_set>
24
25namespace {
26GGeneratedParticleBank make_generated_particle_bank(const GParticleRecordEvent& particles) {
28 bank.reserve(particles.size());
29
30 for (const auto& particle : particles) {
31 bank.push_back({
32 particle.name,
33 particle.pid,
34 particle.type,
35 particle.multiplicity,
36 particle.p,
37 particle.theta,
38 particle.phi,
39 particle.vx,
40 particle.vy,
41 particle.vz
42 });
43 }
44
45 return bank;
46}
47
48GAncestorBank make_ancestor_bank(const std::vector<GTrackRecord>& records) {
49 GAncestorBank bank;
50 bank.reserve(records.size());
51 for (const auto& record : records) {
52 bank.push_back({
53 record.pid,
54 record.tid,
55 record.mtid,
56 record.kinetic_energy,
57 record.momentum.x(),
58 record.momentum.y(),
59 record.momentum.z(),
60 record.vertex.x(),
61 record.vertex.y(),
62 record.vertex.z()
63 });
64 }
65 return bank;
66}
67
68bool scalar_bool_option_enabled(const std::shared_ptr<GOptions>& goptions, const std::string& name) {
69 std::string value = goptions->getOptionalScalarString(name).value_or("");
70 std::transform(value.begin(), value.end(), value.begin(),
71 [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
72 return value == "true" || value == "1" || value == "yes" || value == "on";
73}
74
75// Match a detector against an option containing comma- or whitespace-separated names.
76bool detector_is_listed(const std::shared_ptr<GOptions>& goptions, const std::string& option,
77 const std::string& detector) {
78 std::string detectors = goptions->getOptionalScalarString(option).value_or("");
79 std::replace(detectors.begin(), detectors.end(), ',', ' ');
80
81 std::istringstream names(detectors);
82 std::string name;
83 while (names >> name) {
84 if (name == "all" || name == detector) return true;
85 }
86 return false;
87}
88
89// Convert a substring to a non-negative integer, returning false on any malformed input.
90bool to_non_negative_int(const std::string& text, int& out) {
91 if (text.empty()) return false;
92 size_t pos = 0;
93 int value;
94 try { value = std::stoi(text, &pos); }
95 catch (...) { return false; }
96 if (pos != text.size() || value < 0) return false;
97 out = value;
98 return true;
99}
100}
101
102
103// Construct the event action and keep access to shared configuration plus the
104// non-owning thread-local run action used during event finalization.
105GEventAction::GEventAction(const std::shared_ptr<GOptions>& gopt, GRunAction* run_a,
106 std::shared_ptr<GTrackProvenance> provenance) :
108 goptions(gopt),
109 run_action(run_a),
110 track_provenance(std::move(provenance)) {
111 const auto thread_id = G4Threading::G4GetThreadId();
112 const auto desc = "GEventAction " + std::to_string(thread_id);
113 log->debug(CONSTRUCTOR, "%1%2", FUNCTION_NAME, desc);
114 save_all_ancestors = goptions->getSwitch(SAVE_ALL_ANCESTORS_SWITCH);
115 save_original_track = goptions->getSwitch(SAVE_ORIGINAL_TRACK_SWITCH) || save_all_ancestors;
116
117 // Parse the log_every option of the form N or N-NTH. Anything malformed disables the
118 // feature and is reported once (from thread 0) to avoid duplicated warnings across workers.
119 const auto spec_option = goptions->getOptionalScalarString(LOG_EVERY_OPTION);
120 if (spec_option && !spec_option->empty()) {
121 const std::string& spec = *spec_option;
122 const auto dash = spec.find('-');
123 std::string n_part = dash == std::string::npos ? spec : spec.substr(0, dash);
124 std::string nth_part = dash == std::string::npos ? std::string() : spec.substr(dash + 1);
125
126 // Effective worker-thread count, mirroring gemc::get_nthreads clamping (0 means all cores).
127 int nthreads = goptions->getRequiredScalarInt("nthreads");
128 const int ncores = G4Threading::G4GetNumberOfCores();
129 if (nthreads == 0 || nthreads > ncores) nthreads = ncores;
130
131 int n = 0;
132 if (!to_non_negative_int(n_part, n) || n == 0) {
133 if (thread_id <= 0)
134 log->warning("Ignoring invalid -%1=%2 : N must be a positive integer.", LOG_EVERY_OPTION, spec);
135 }
136 else if (dash != std::string::npos) {
137 int nth = 0;
138 if (!to_non_negative_int(nth_part, nth) || nth >= nthreads) {
139 if (thread_id <= 0)
140 log->warning(
141 "Ignoring invalid -%1=%2 : thread id must be in [0, %3].", LOG_EVERY_OPTION, spec,
142 nthreads - 1);
143 }
144 else {
145 log_every_n = n;
146 log_every_thread = nth;
147 }
148 }
149 else { log_every_n = n; }
150 }
151}
152
153// Print the periodic "Starting event" line, honoring the log module and optional thread filter.
154// The reported event number, count and rate are all per worker thread: each enabled thread logs
155// every N events it processes, showing its own 1-based event count and average rate (events / second).
156void GEventAction::log_event_start(int thread_id) {
157 if (log_every_n <= 0) return;
158 if (log_every_thread >= 0 && log_every_thread != thread_id) return;
159
160 // Anchor this thread's clock on its first counted event, then count this event.
161 const auto now = std::chrono::steady_clock::now();
162 if (log_events_seen == 0) { log_start_time = now; }
163 ++log_events_seen;
164
165 if (log_events_seen % log_every_n != 0) return;
166
167 const double elapsed_s = std::chrono::duration<double>(now - log_start_time).count();
168 const double rate = elapsed_s > 0.0 ? static_cast<double>(log_events_seen) / elapsed_s : 0.0;
169
170 // log_events_seen is this thread's own 1-based count, not the global Geant4 event id.
171 log->info(
172 0, "Starting event n. %1 in thread %2. Average rate: %3 events / second", log_events_seen, thread_id,
173 rate);
174}
175
176// Begin-of-event hook used mainly for tracing event and thread identifiers.
177void GEventAction::BeginOfEventAction([[maybe_unused]] const G4Event* event) {
178 const auto thread_id = G4Threading::G4GetThreadId();
179 const auto event_id = event->GetEventID();
180
181 log->debug(NORMAL, "%1 event id %2 in thread %3", FUNCTION_NAME, event_id, thread_id);
182 if (track_provenance != nullptr) { track_provenance->clear(); }
183
184 log_event_start(thread_id);
185}
186
187// Finalize the event by reading hit collections, digitizing them, routing the
188// resulting payload according to collection mode, and publishing event-mode output.
189void GEventAction::EndOfEventAction([[maybe_unused]] const G4Event* event) {
190 if (run_action == nullptr) {
191 log->error(
193 "%1 run_action is null - cannot access digitization routines or streamers.", FUNCTION_NAME);
194 return;
195 }
196
197 // Count each processed event once, even when it produces no payload.
198 run_action->increment_run_events_processed();
199 stream_event(event);
200
201 const auto thread_id = G4Threading::G4GetThreadId();
202 const auto event_id = event->GetEventID();
203
204 auto gevent_header = std::make_unique<GEventHeader>(goptions, event_id, thread_id);
205 auto eventDataCollection = std::make_shared<GEventDataCollection>(goptions, std::move(gevent_header));
206 eventDataCollection->setGeneratedParticles(
207 make_generated_particle_bank(GPrimaryGeneratorAction::currentGeneratedParticleRecords()));
208 eventDataCollection->setGeneratedTrackedParticles(
210
211 auto* const hcs_this_event = event->GetHCofThisEvent();
212 if (hcs_this_event == nullptr) {
213 if (!eventDataCollection->getGeneratedParticles().empty() ||
214 !eventDataCollection->getGeneratedTrackedParticles().empty()) {
215 publish_event_data(eventDataCollection);
216 }
217 return;
218 }
219
220 const auto digi_map = run_action->get_digitization_routines_map();
221 if (digi_map == nullptr) {
222 log->error(
223 gaction::ERR_GDIGIMAP_NOT_EXISTING, "%1 no digitization routines map available in thread %2",
224 FUNCTION_NAME, thread_id);
225 return;
226 }
227
228 bool has_event_mode_payload = false;
229 bool has_run_mode_payload = false;
230 const bool also_reject_true_info = scalar_bool_option_enabled(goptions, "also_reject_true_info");
231 std::unordered_set<int> ancestor_track_ids;
232
233 // Loop over every hit collection produced during this event and dispatch each
234 // collection to the digitization routine registered under its collection name.
235 for (G4int hci = 0; hci < hcs_this_event->GetNumberOfCollections(); ++hci) {
236 auto* const this_ghc = static_cast<GHitsCollection*>(hcs_this_event->GetHC(hci));
237 if (this_ghc == nullptr) {
238 continue;
239 }
240
241 const std::string hcSDName = this_ghc->GetSDname();
242 const bool no_digitized = detector_is_listed(goptions, NO_DIGITIZED_OPTION, hcSDName);
243 const bool no_true_info = detector_is_listed(goptions, NO_TRUE_INFO_OPTION, hcSDName);
244
245 log->info(
246 2, "%1 worker %2 for event number %3 for collection number %4 collection name: %5", FUNCTION_NAME,
247 thread_id, event_id, hci + 1, hcSDName);
248
249 // Resolve the digitization routine responsible for this collection.
250 const auto it = digi_map->find(hcSDName);
251 if (it == digi_map->end()) {
252 log->error(
254 "%1 no digitization routine registered for collection %2 in thread %3", FUNCTION_NAME, hcSDName,
255 thread_id);
256 continue;
257 }
258
259 const auto& digitization_routine = it->second;
260 if (digitization_routine == nullptr) {
261 log->error(
263 "%1 digitization routine is null for collection %2 in thread %3", FUNCTION_NAME, hcSDName,
264 thread_id);
265 continue;
266 }
267
268 const auto collection_mode = digitization_routine->collection_mode();
269 size_t accepted_hit_index = 0;
270
271 // Process all hits in the collection. Event-mode digitizers append to the
272 // event container, while run-mode digitizers append to the run container.
273 for (size_t hitIndex = 0; hitIndex < this_ghc->GetSize(); ++hitIndex) {
274
275 auto* const this_hit = static_cast<GHit*>(this_ghc->GetHit(hitIndex));
276 if (this_hit == nullptr) {
277 continue;
278 }
279 if (save_all_ancestors) {
280 const auto track_ids = this_hit->getTids();
281 ancestor_track_ids.insert(track_ids.begin(), track_ids.end());
282 }
283
284 auto digi_data = no_digitized ? nullptr : digitization_routine->digitizeHit(this_hit, hitIndex);
285 bool hit_accepted = digi_data != nullptr;
286
287 // Apply post-digitization threshold and efficiency policies. Plugins may declare a
288 // policy intrinsic or leave it controlled by -applyThresholds / -applyInefficiencies.
289 // Both are evaluated so the detector's random-number sequence remains stable.
290 //
291 // An intrinsic policy reproduces a rejection baked into the GEMC2 digitization (e.g. the
292 // ECAL FADC threshold), where a rejected hit is dropped from every output bank. Track when
293 // the rejection came from such a policy so the true-info row is dropped too, matching GEMC2
294 // regardless of the global also_reject_true_info flag. Global-option-driven rejection of a
295 // non-intrinsic plugin keeps the legacy behaviour (true info governed by that flag).
296 bool rejected_by_intrinsic_policy = false;
297 if (hit_accepted) {
298 const bool skip_threshold = digitization_routine->apply_thresholds(this_hit, digi_data.get());
299 const bool skip_efficiency = digitization_routine->apply_efficiency(this_hit, digi_data.get());
300 if (skip_threshold || skip_efficiency) {
301 digi_data.reset();
302 hit_accepted = false;
303 rejected_by_intrinsic_policy =
304 (skip_threshold && digitization_routine->thresholds_are_intrinsic_impl()) ||
305 (skip_efficiency && digitization_routine->efficiencies_are_intrinsic_impl());
306 }
307 }
308
309 if (collection_mode == CollectionMode::event) {
310 if (hit_accepted) {
311 ++accepted_hit_index;
312 digi_data->includeVariable("hitn", static_cast<int>(accepted_hit_index));
313 run_action->record_analysis_digitized(hcSDName, *digi_data);
314 eventDataCollection->addDetectorDigitizedData(hcSDName, std::move(digi_data));
315 has_event_mode_payload = true;
316 }
317 }
318 else if (collection_mode == CollectionMode::run) {
319 if (hit_accepted) {
320 run_action->record_analysis_digitized(hcSDName, *digi_data);
321 run_action->collect_event_data_collections(
322 hcSDName,
323 std::move(digi_data));
324 has_run_mode_payload = true;
325 }
326 }
327
328 // Event output already requires true information. In GUI analysis mode, request it for
329 // run-mode plugins too so their runtime-defined variables can be discovered without an API schema.
330 // A hit rejected by an intrinsic policy drops its true-info row like GEMC2 did, in addition to
331 // the global also_reject_true_info opt-in.
332 const bool drop_true_for_rejected_hit = also_reject_true_info || rejected_by_intrinsic_policy;
333 const bool collect_true = !no_true_info &&
334 (no_digitized || hit_accepted || !drop_true_for_rejected_hit) &&
335 (collection_mode == CollectionMode::event || run_action->analysis_enabled());
336 if (collect_true) {
337 const size_t output_hit_index = collection_mode == CollectionMode::event && hit_accepted
338 ? accepted_hit_index : hitIndex + 1;
339 auto true_data = digitization_routine->collectTrueInformation(this_hit, output_hit_index);
340 if (save_original_track && track_provenance != nullptr && true_data != nullptr) {
341 const int tid = this_hit->getTid();
342 const G4ThreeVector op = track_provenance->originalTrackMomentum(tid);
343 true_data->includeVariable("otid", track_provenance->originalTrackId(tid));
344 true_data->includeVariable("opid", track_provenance->originalTrackPid(tid));
345 true_data->includeVariable("opx", op.getX());
346 true_data->includeVariable("opy", op.getY());
347 true_data->includeVariable("opz", op.getZ());
348 }
349 if (true_data != nullptr) { run_action->record_analysis_true(hcSDName, *true_data); }
350 if (collection_mode == CollectionMode::event) {
351 eventDataCollection->addDetectorTrueInfoData(hcSDName, std::move(true_data));
352 has_event_mode_payload = true;
353 }
354 }
355 }
356 }
357
358 if (save_all_ancestors && track_provenance != nullptr) {
359 eventDataCollection->setAncestors(
360 make_ancestor_bank(track_provenance->ancestorsForTracks(ancestor_track_ids)));
361 }
362
363 // Record whether this event contributed at least one run-mode payload entry.
364 if (has_run_mode_payload) {
365 run_action->increment_run_events_with_payload();
366 }
367
368 // Publish event-mode output once, after all collections have been processed.
369 if (has_event_mode_payload ||
370 !eventDataCollection->getAncestors().empty() ||
371 !eventDataCollection->getGeneratedParticles().empty() ||
372 !eventDataCollection->getGeneratedTrackedParticles().empty()) {
373 publish_event_data(eventDataCollection);
374 }
375}
376
377void GEventAction::stream_event(const G4Event* event) {
378 const auto& sro = run_action->get_sro_factory();
379 if (!sro) { return; }
380 if (event->IsAborted()) {
381 sro->interrupt_run(); // Never complete an event whose hit collection may be truncated.
382 return;
383 }
384 try {
385 sro->rethrow_if_failed();
386 const auto context = sro->event_context(static_cast<GSROEventId>(event->GetEventID()));
387 std::uint64_t sequence = 0;
388 const GSROEmit emit = [&](GSROCrateId crate, GSROTime time, std::unique_ptr<const GSROData> data) {
389 if (sequence == std::numeric_limits<std::uint64_t>::max()) {
390 throw std::overflow_error("SRO payload sequence overflow");
391 }
392 sro->dispatch_payload_to_crate({crate, context.event_id, sequence++, time, std::move(data)});
393 };
394 if (auto* collections = event->GetHCofThisEvent()) {
395 const auto routines = run_action->get_digitization_routines_map();
396 if (!routines) { throw std::runtime_error("SRO requires a digitization routine map"); }
397 for (int index = 0; index < collections->GetNumberOfCollections(); ++index) {
398 auto* hits = static_cast<GHitsCollection*>(collections->GetHC(index));
399 if (!hits || detector_is_listed(goptions, NO_DIGITIZED_OPTION, hits->GetSDname())) { continue; }
400 const auto& routine = routines->at(hits->GetSDname());
401 if (!routine) { throw std::runtime_error("Null SRO digitization routine"); }
402 for (std::size_t hit = 0; hit < hits->GetSize(); ++hit) {
403 if (auto* data = static_cast<GHit*>(hits->GetHit(hit))) {
404 routine->stream_hit(data, hit, context, emit);
405 }
406 }
407 }
408 }
409 sro->complete_event(context.event_id);
410 }
411 catch (...) {
412 sro->cancel_run(std::current_exception());
413 // Leave joining and checked output finalization to the run owner after workers return.
414 if (auto* manager = G4RunManager::GetRunManager()) { manager->AbortRun(true); }
415 }
416}
417
418// Send the completed event-data object to every configured worker-thread streamer.
419void GEventAction::publish_event_data(const std::shared_ptr<GEventDataCollection>& event_data) const {
420 if (run_action == nullptr || event_data == nullptr) {
421 return;
422 }
423
424 if (!run_action->has_streamer_threads_map()) {
425 return;
426 }
427
428 const auto gstreamers_threads_map = run_action->get_streamer_threads_map();
429 if (gstreamers_threads_map == nullptr) {
430 log->error(
432 "%1 no thread streamer map available - event will not be published.", FUNCTION_NAME);
433 return;
434 }
435
436 for (const auto& [name, gstreamer] : *gstreamers_threads_map) {
437 if (gstreamer == nullptr) {
438 log->error(
439 gaction::ERR_STREAMERMAP_NOT_EXISTING, "%1 null gstreamer instance for streamer %2",
440 FUNCTION_NAME, name);
441 continue;
442 }
443
444 gstreamer->publishEventData(event_data);
445 }
446}
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:71
const std::shared_ptr< GSROFactory > & get_sro_factory() const
Definition gRunAction.h:88
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.
std::function< void(GSROCrateId, GSROTime, std::unique_ptr< const GSROData >)> GSROEmit
std::chrono::duration< std::int64_t, std::nano > GSROTime
std::uint32_t GSROCrateId
std::uint64_t GSROEventId
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