Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/geant4_integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 },
},
},
}
Expand Down
70 changes: 57 additions & 13 deletions src/geant4_module.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,10 @@
#include <SHiP/SimResult.hpp>
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cmath>
#include <cstddef>
#include <exception>
#include <filesystem>
#include <map>
#include <memory>
Expand Down Expand Up @@ -111,14 +113,16 @@ 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 {
public:
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
Expand Down Expand Up @@ -147,8 +151,23 @@ class Geant4Sim {
std::vector<SHiP::MCParticle> 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();

Expand Down Expand Up @@ -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<std::size_t>(cfg_.progress_interval) == 0) {
auto const elapsed = std::chrono::duration<double>(
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;
}

Expand All @@ -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(
Expand All @@ -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_);

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<ship::IFieldSource> field_; // outlives G4 run
std::atomic<int> next_thread_id_{0};
std::atomic<int> 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<std::size_t> 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
Expand Down Expand Up @@ -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<std::string>("export_gdml", std::string{}),
.progress_interval = config.get<int>("progress_interval", 100),
};

if (cfg.concurrency > active_parallelism)
Expand Down
2 changes: 1 addition & 1 deletion workflows/lib.libsonnet
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
Loading