diff --git a/docs/geant4_integration.md b/docs/geant4_integration.md index 8ef7f25..b1b2109 100644 --- a/docs/geant4_integration.md +++ b/docs/geant4_integration.md @@ -109,6 +109,7 @@ unloading, causing crashes. This is a known Geant4 limitation. | `particle_ke_cut` | double | `0.0` | KE below which secondary particles are not recorded (GeV) | | `regions` | map | `{}` | Volume name pattern to production cut (mm) mapping | | `export_gdml` | string | *(unset)* | Write the constructed geometry to this GDML file after initialisation. Errors if the file exists. Lets external tools (e.g. the GENIE event generator) use exactly the geometry Geant4 tracks in | +| `progress_interval` | int | `100` | Log a progress line (event count and average rate) every this many simulated events; `0` disables | For consumers that read the exported file with ROOT's TGeo importer, run `scripts/gdml_fix_element_names.py` on it first: ROOT confuses materials @@ -133,7 +134,7 @@ Example workflow configuration: ke_threshold: 0.5, energy_cut: true, particle_ke_cut: 1.0, - regions: { Target: 50, HadronAbsorber: 50 }, + regions: { '/SHiP/target': 50, '/SHiP/muon_shield/magn_absorb': 50 }, }, }, } diff --git a/src/geant4_module.cpp b/src/geant4_module.cpp index 6e0ce57..8a1a0f4 100644 --- a/src/geant4_module.cpp +++ b/src/geant4_module.cpp @@ -45,8 +45,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -111,6 +113,8 @@ struct Geant4SimConfig { // When set, write the constructed geometry to this GDML file after // initialisation (e.g. to feed the same geometry to external tools). std::string export_gdml; + // Log a progress line every this many simulated events (0 disables). + int progress_interval = 100; }; class Geant4Sim { @@ -118,7 +122,7 @@ class Geant4Sim { explicit Geant4Sim(Geant4SimConfig cfg) : cfg_{std::move(cfg)} {} ~Geant4Sim() { - if (!initialized_) return; + if (!master_constructed_) return; // Empty the geometry stores now, on the geometry thread. Their static // singletons' destructors run at process exit on the program's main // thread, which never initialised Geant4's thread-local split-class @@ -147,8 +151,23 @@ class Geant4Sim { std::vector const& particles) { AEGIR_TRACE_EVENT("g4", "simulate"); - std::call_once(init_flag_, - [this, &geo, &field]() { init_master(geo, field); }); + // A failed master init is sticky: retrying would construct a second + // G4MTRunManager next to the leaked first one and die on Geant4's fatal + // "constructed twice" abort, masking the original error. Store the + // exception instead and rethrow it on this and every later call — phlex + // logs it at shutdown and terminates the job cleanly. + std::call_once(init_flag_, [this, &geo, &field]() { + try { + init_master(geo, field); + } catch (std::exception const& e) { + spdlog::error("geant4_module: master initialisation failed: {}", + e.what()); + init_error_ = std::current_exception(); + } catch (...) { + init_error_ = std::current_exception(); + } + }); + if (init_error_) std::rethrow_exception(init_error_); if (!tl_kernel) init_worker(); @@ -232,6 +251,17 @@ class Geant4Sim { result.hits = std::move(tl_hits); result.particles = std::move(tl_particles); } + + auto const done = completed_.fetch_add(1) + 1; + if (cfg_.progress_interval > 0 && + done % static_cast(cfg_.progress_interval) == 0) { + auto const elapsed = std::chrono::duration( + std::chrono::steady_clock::now() - sim_start_) + .count(); + spdlog::info("geant4_module: {} events simulated ({:.1f} events/s)", done, + elapsed > 0 ? done / elapsed : 0.0); + } + return result; } @@ -243,10 +273,8 @@ class Geant4Sim { // pre-check the cases we can to fail with a catchable, clear error // instead. Only the existing-file and missing-parent-directory cases are // validated here — other fatal write errors (e.g. a read-only directory) - // remain Geant4's responsibility. Doing this before the G4MTRunManager is - // constructed keeps the std::call_once retry clean: the run manager is a - // leaked singleton, so a throw after it exists would make the retry abort - // with "run manager already exists". + // remain Geant4's responsibility. Failing before the G4MTRunManager is + // constructed keeps the failure cheap: nothing has been built yet. if (!cfg_.export_gdml.empty()) { if (std::filesystem::exists(cfg_.export_gdml)) throw std::runtime_error( @@ -268,12 +296,13 @@ class Geant4Sim { // slot count is shared while its data array is thread-local), and other // plugins — e.g. aegir-genie's GENIE geometry analyzer — create theirs // on this thread too (aegir-genie issue #11, Geant4 bug #2747). The call - // blocks until initialisation is done; exceptions propagate, so a failed - // init can be retried (init_flag_ stays unset). + // blocks until initialisation is done; exceptions propagate to the + // caller in simulate(), which records them as a sticky init failure. ship::geometry_thread().run([this] { AEGIR_TRACE_THREAD_NAME("g4_master"); AEGIR_TRACE_EVENT("g4", "init_master"); auto* rm = new G4MTRunManager(); + master_constructed_ = true; rm->SetNumberOfThreads(cfg_.concurrency); rm->SetUserInitialization(detector_); @@ -313,7 +342,7 @@ class Geant4Sim { // The run manager stays alive (and leaked) for the whole process. }); - initialized_ = true; + sim_start_ = std::chrono::steady_clock::now(); spdlog::info("Geant4 direct simulation ready ({} worker slots)", cfg_.concurrency); @@ -347,15 +376,29 @@ class Geant4Sim { Geant4SimConfig cfg_; std::once_flag init_flag_; + // Written at most once inside the call_once body, read only after the + // call_once returns — no further synchronisation needed. + std::exception_ptr init_error_; G4VPhysicalVolume* world_pv_ = nullptr; G4VUserPhysicsList* physics_list_ = nullptr; ConfigurableDetectorConstruction* detector_ = nullptr; // owned by G4 std::shared_ptr field_; // outlives G4 run std::atomic next_thread_id_{0}; std::atomic next_event_id_{0}; - // Set only after a successful init_master, so the destructor cleans the - // stores exactly when there is something to clean. - bool initialized_ = false; + // Completed-event count (completion order, not event id, so the logged + // count is monotonic) and the reference point for the average event rate. + // sim_start_ is written in init_master and published by the call_once + // every simulate call passes through first. + std::atomic completed_{0}; + std::chrono::steady_clock::time_point sim_start_; + // Set on the geometry thread as soon as the G4MTRunManager exists. From + // that point the geometry stores may hold volumes — even when a later init + // step throws, detector construction has typically already registered them + // — so the destructor must clean the stores on the geometry thread (#68) + // for failed and successful inits alike. Guarding on full init success + // instead would leave the stores populated after a failed init and their + // static destructors would segfault at process exit. + bool master_constructed_ = false; }; } // namespace @@ -396,6 +439,7 @@ PHLEX_REGISTER_ALGORITHMS(m, config) { aegir::get_quantity(config, "particle_ke_cut", 0.0 * su::GeV), .regions = std::move(regions), .export_gdml = config.get("export_gdml", std::string{}), + .progress_interval = config.get("progress_interval", 100), }; if (cfg.concurrency > active_parallelism) diff --git a/workflows/lib.libsonnet b/workflows/lib.libsonnet index 4b050ab..c27cfe4 100644 --- a/workflows/lib.libsonnet +++ b/workflows/lib.libsonnet @@ -98,7 +98,7 @@ ke_threshold: 0.5, energy_cut: true, particle_ke_cut: 1.0, - regions: { Target: 50, HadronAbsorber: 50 }, + regions: { '/SHiP/target': 50, '/SHiP/muon_shield/magn_absorb': 50 }, }, noop_output:: { cpp: 'sim_output_module', mode: 'noop' },