From cf28522d587539a25c19b58c5b0fd6bfee2fe7cb Mon Sep 17 00:00:00 2001 From: Seth R Johnson Date: Mon, 12 Jan 2026 12:14:30 -0500 Subject: [PATCH 1/3] Add high-level optical surface integration test --- test/accel/CMakeLists.txt | 14 +- test/accel/IntegrationTestBase.cc | 20 ++ test/accel/IntegrationTestBase.hh | 8 + test/accel/TrackingManagerIntegration.test.cc | 257 +++++++++++++----- 4 files changed, 223 insertions(+), 76 deletions(-) diff --git a/test/accel/CMakeLists.txt b/test/accel/CMakeLists.txt index 6ddd92f3f3..cdd27d70a8 100644 --- a/test/accel/CMakeLists.txt +++ b/test/accel/CMakeLists.txt @@ -102,13 +102,16 @@ celeritas_add_integration_tests( RMTYPE serial mt ) +#-----------------------------------------------------------------------------# +# Disable MT Geant4 geometry for optical physics +#-----------------------------------------------------------------------------# + set(_optical_rm_type "serial") if(NOT (CELERITAS_CORE_GEO STREQUAL "Geant4")) list(APPEND _optical_rm_type "mt") endif() -# Test with optical physics, except that currently trying to use Geant4 -# navigation for the optical tracks wreaks havoc +# Test optical celeritas_add_integration_tests( TrackingManagerIntegration LarSphereOptical run OFFLOAD cpu gpu g4 @@ -122,4 +125,11 @@ celeritas_add_integration_tests( RMTYPE ${_optical_rm_type} ) +# Test optical surface physics +celeritas_add_integration_tests( + TrackingManagerIntegration OpticalSurfaces run + OFFLOAD cpu gpu g4 + RMTYPE ${_optical_rm_type} +) + #-----------------------------------------------------------------------------# diff --git a/test/accel/IntegrationTestBase.cc b/test/accel/IntegrationTestBase.cc index 7401545e22..c351553ca8 100644 --- a/test/accel/IntegrationTestBase.cc +++ b/test/accel/IntegrationTestBase.cc @@ -382,6 +382,26 @@ auto IntegrationTestBase::make_sens_det(std::string const&) -> UPSensDet return nullptr; } +//---------------------------------------------------------------------------// +// FREE FUNCTIONS +//---------------------------------------------------------------------------// +void enable_optical_physics(IntegrationTestBase::PhysicsInput& phys_inp) +{ + // Set default optical physics + auto& optical = phys_inp.optical; + optical = {}; + EXPECT_TRUE(optical); + EXPECT_TRUE(optical.cherenkov); + EXPECT_TRUE(optical.scintillation); + + // Disable WLS which isn't yet working (reemission) in Celeritas + using WLSO = WavelengthShiftingOptions; + optical.wavelength_shifting = WLSO::deactivated(); + optical.wavelength_shifting2 = WLSO::deactivated(); +} + +//---------------------------------------------------------------------------// +// TEST PROBLEM MIXINS //---------------------------------------------------------------------------// /*! * Create physics list: default is EM only using make_physics_input. diff --git a/test/accel/IntegrationTestBase.hh b/test/accel/IntegrationTestBase.hh index 13876004af..877f52f78e 100644 --- a/test/accel/IntegrationTestBase.hh +++ b/test/accel/IntegrationTestBase.hh @@ -109,6 +109,14 @@ class IntegrationTestBase : public ::celeritas::test::Test //!@} }; +//---------------------------------------------------------------------------// +// FREE FUNCTIONS +//---------------------------------------------------------------------------// + +void enable_optical_physics(IntegrationTestBase::PhysicsInput&); + +//---------------------------------------------------------------------------// +// TEST PROBLEM MIXINS //---------------------------------------------------------------------------// //! Generate LAr sphere geometry with 10 MeV electrons and functional hit call class LarSphereIntegrationMixin : virtual public IntegrationTestBase diff --git a/test/accel/TrackingManagerIntegration.test.cc b/test/accel/TrackingManagerIntegration.test.cc index 7b90f12238..d8d5a5da98 100644 --- a/test/accel/TrackingManagerIntegration.test.cc +++ b/test/accel/TrackingManagerIntegration.test.cc @@ -14,11 +14,13 @@ #include #include +#include "corecel/cont/Array.hh" #include "corecel/io/Logger.hh" #include "geocel/GeantUtils.hh" #include "geocel/UnitUtils.hh" #include "celeritas/ext/GeantParticleView.hh" #include "celeritas/global/CoreState.hh" +#include "celeritas/inp/Events.hh" #include "celeritas/optical/CoreState.hh" #include "celeritas/optical/OpticalCollector.hh" #include "celeritas/phys/PDGNumber.hh" @@ -50,6 +52,39 @@ constexpr bool using_surface_vg = CELERITAS_VECGEOM_SURFACE && CELERITAS_CORE_GEO == CELERITAS_CORE_GEO_VECGEOM; +/*! + * Count particle types. + */ +class CounterTrackingAction final : public G4UserTrackingAction +{ + public: + void PreUserTrackingAction(G4Track const* t) final + { + GeantParticleView particle{*t->GetParticleDefinition()}; + + if (particle.pdg() == pdg::electron()) + { + ++num_electrons_; + } + if (particle.pdg() == pdg::positron()) + { + ++num_positrons_; + } + else if (particle.is_optical_photon()) + { + ++num_photons_; + } + } + std::size_t num_photons() const { return num_photons_; } + std::size_t num_electrons() const { return num_electrons_; } + std::size_t num_positrons() const { return num_positrons_; } + + private: + std::size_t num_photons_{}; + std::size_t num_electrons_{}; + std::size_t num_positrons_{}; +}; + } // namespace //---------------------------------------------------------------------------// @@ -101,6 +136,8 @@ class TMITestBase : virtual public IntegrationTestBase std::function check_during_run_; }; +//---------------------------------------------------------------------------// +// LAR SPHERE //---------------------------------------------------------------------------// class LarSphere : public LarSphereIntegrationMixin, public TMITestBase { @@ -215,54 +252,36 @@ TEST_F(LarSphere, run_ui) // LAR SPHERE WITH OPTICAL //---------------------------------------------------------------------------// /*! - * Count particle types. - * - * \todo This is redundant with (but more "Geant4-like" than) - * \c GeantStepDiagnostic . + * Test the LarSphere, offloading both EM tracks *and* optical photons. */ -class TrackingAction : public G4UserTrackingAction +class LarSphereOptical : public LarSphere { public: - void PreUserTrackingAction(G4Track const* t) + PhysicsInput make_physics_input() const override { - GeantParticleView particle{*t->GetParticleDefinition()}; - - if (particle.pdg() == pdg::electron()) - { - ++num_electrons_; - } - if (particle.pdg() == pdg::positron()) - { - ++num_positrons_; - } - else if (particle.is_optical_photon()) - { - ++num_photons_; - } + auto result = LarSphere::make_physics_input(); + enable_optical_physics(result); + return result; } - std::size_t num_photons() const { return num_photons_; } - std::size_t num_electrons() const { return num_electrons_; } - std::size_t num_positrons() const { return num_positrons_; } - private: - std::size_t num_photons_{}; - std::size_t num_electrons_{}; - std::size_t num_positrons_{}; -}; + PrimaryInput make_primary_input() const override + { + auto result = LarSphereIntegrationMixin::make_primary_input(); + + result.shape = inp::PointDistribution{ + array_cast(from_cm({0.1, 0.1, 0}))}; + result.primaries_per_event = 1; + result.energy = inp::MonoenergeticDistribution{2}; // [MeV] + return result; + } -/*! - * Test the LarSphere, offloading both EM tracks *and* optical photons. - */ -class LarSphereOptical : public LarSphere -{ - public: - PhysicsInput make_physics_input() const override; - PrimaryInput make_primary_input() const override; SetupOptions make_setup_options() override; + void EndOfRunAction(G4Run const* run) override; + UPTrackAction make_tracking_action() override { - auto result = std::make_unique(); + auto result = std::make_unique(); { // Store the raw pointer in the tracking_ vector using a static // mutex @@ -274,43 +293,9 @@ class LarSphereOptical : public LarSphere } private: - std::vector tracking_; + std::vector tracking_; }; -//---------------------------------------------------------------------------// -/*! - * Enable optical physics. - */ -auto LarSphereOptical::make_physics_input() const -> PhysicsInput -{ - auto result = LarSphereIntegrationMixin::make_physics_input(); - - // Set default optical physics - auto& optical = result.optical; - optical = {}; - EXPECT_TRUE(optical); - EXPECT_TRUE(optical.cherenkov); - EXPECT_TRUE(optical.scintillation); - - // Disable WLS which isn't yet working (reemission) in Celeritas - using WLSO = WavelengthShiftingOptions; - optical.wavelength_shifting = WLSO::deactivated(); - optical.wavelength_shifting2 = WLSO::deactivated(); - - return result; -} - -auto LarSphereOptical::make_primary_input() const -> PrimaryInput -{ - auto result = LarSphereIntegrationMixin::make_primary_input(); - - result.shape - = inp::PointDistribution{array_cast(from_cm({0.1, 0.1, 0}))}; - result.primaries_per_event = 1; - result.energy = inp::MonoenergeticDistribution{2}; // [MeV] - return result; -} - //---------------------------------------------------------------------------// /*! * Enable optical tracking. @@ -416,6 +401,9 @@ TEST_F(LarSphereOptical, run) rm.BeamOn(2); } +//---------------------------------------------------------------------------// +// OPNOVICE +//---------------------------------------------------------------------------// /*! * Test the Op-Novice example, offloading optical photons. */ @@ -425,7 +413,7 @@ class OpNoviceOptical : public OpNoviceIntegrationMixin, public TMITestBase void EndOfRunAction(G4Run const* run) override; UPTrackAction make_tracking_action() override { - auto result = std::make_unique(); + auto result = std::make_unique(); { // Store the raw pointer in the tracking_ vector using a static // mutex @@ -437,7 +425,7 @@ class OpNoviceOptical : public OpNoviceIntegrationMixin, public TMITestBase } private: - std::vector tracking_; + std::vector tracking_; }; //---------------------------------------------------------------------------// @@ -526,6 +514,127 @@ TEST_F(OpNoviceOptical, run) rm.BeamOn(10); } +//---------------------------------------------------------------------------// +// OPTICAL SURFACES +//---------------------------------------------------------------------------// +/*! + * Test the LarSphere, offloading both EM tracks *and* optical photons. + */ +class OpticalSurfaces : public TMITestBase +{ + public: + std::string_view gdml_basename() const final { return "optical-surfaces"; } + PhysicsInput make_physics_input() const override + { + auto result = TMITestBase::make_physics_input(); + enable_optical_physics(result); + return result; + } + + PrimaryInput make_primary_input() const override; + SetupOptions make_setup_options() override; + void EndOfRunAction(G4Run const* run) override; +}; + +//---------------------------------------------------------------------------// +/*! + * Fire positrons through the liquid argon toward the detectors. + */ +auto OpticalSurfaces::make_primary_input() const -> PrimaryInput +{ + PrimaryInput result; + result.pdg = {pdg::positron()}; + result.shape + = inp::PointDistribution{array_cast(from_cm({30, 0, 0}))}; + result.angle = inp::MonodirectionalDistribution{{-1, 0, 0}}; + result.energy = inp::MonoenergeticDistribution{10}; // [MeV] + result.primaries_per_event = 1; + result.num_events = 4; // Overridden with BeamOn + return result; +} + +//---------------------------------------------------------------------------// +/*! + * Enable optical tracking. + */ +auto OpticalSurfaces::make_setup_options() -> SetupOptions +{ + auto result = TMITestBase::make_setup_options(); + + result.sd.enabled = false; + result.optical = [] { + OpticalSetupOptions opt; + opt.capacity.tracks = 32768; + opt.capacity.generators = 32768 * 8; + opt.capacity.primaries = opt.capacity.generators; + return opt; + }(); + + return result; +} + +//---------------------------------------------------------------------------// +/*! + * Test that the optical tracking loop completed correctly. + * + * - Generator counters show whether any photons are queued but not run + * - Accumulated stats show whether the state has run some photons + */ +void OpticalSurfaces::EndOfRunAction(G4Run const* run) +{ + auto& integration = detail::IntegrationSingleton::instance(); + if (integration.mode() == OffloadMode::enabled) + { + auto& local_transporter = integration.local_transporter(); + auto const& shared_params = integration.shared_params(); + + // Check that local/shared data is available before end of run + EXPECT_EQ(is_running_events(), static_cast(local_transporter)); + EXPECT_TRUE(shared_params) << "Celeritas was not enabled"; + + auto const& optical_collector = shared_params.optical_collector(); + EXPECT_TRUE(optical_collector) << "optical offloading was not enabled"; + if (local_transporter && optical_collector) + { + // Use diagnostic methods to check counters + auto const& accum_stats + = optical_collector->optical_state(local_transporter.GetState()) + .accum(); + CELER_LOG_LOCAL(info) + << "Ran " << accum_stats.steps << " over " + << accum_stats.step_iters << " step iterations from " + << accum_stats.flushes << " flushes"; + EXPECT_GT(accum_stats.steps, 0); + EXPECT_GT(accum_stats.step_iters, 0); + EXPECT_GT(accum_stats.flushes, 0); + + auto& aux_state = local_transporter.GetState().aux(); + auto counts = optical_collector->buffer_counts(aux_state); + EXPECT_EQ(0, counts.buffer_size); //!< Pending generators + EXPECT_EQ(0, counts.num_pending); //!< Photons pending generation + EXPECT_EQ(0, counts.num_generated); //!< Photons generated + } + } + + // Continue cleanup and other checks at end of run + TMITestBase::EndOfRunAction(run); +} + +//---------------------------------------------------------------------------// +/*! + * Check that the test runs. + */ +TEST_F(OpticalSurfaces, run) +{ + auto& rm = this->run_manager(); + TMI::Instance().SetOptions(this->make_setup_options()); + + CELER_LOG(status) << "Run initialization"; + rm.Initialize(); + CELER_LOG(status) << "Run two events"; + rm.BeamOn(2); +} + //---------------------------------------------------------------------------// // TESTEM3 //---------------------------------------------------------------------------// From a4fe12a15262895cb15a7b23143b7af354e1fdd7 Mon Sep 17 00:00:00 2001 From: Seth R Johnson Date: Mon, 12 Jan 2026 12:29:59 -0500 Subject: [PATCH 2/3] Allow optical materials to be unused by geometry volumes --- src/celeritas/optical/MaterialParams.cc | 43 +++++++++---------------- 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/src/celeritas/optical/MaterialParams.cc b/src/celeritas/optical/MaterialParams.cc index 0b2ad11622..87f24a6908 100644 --- a/src/celeritas/optical/MaterialParams.cc +++ b/src/celeritas/optical/MaterialParams.cc @@ -52,43 +52,32 @@ MaterialParams::from_import(ImportData const& data, inp.properties.push_back(opt_mat.properties); } - // Construct volume-to-optical mapping - inp.volume_to_mat.reserve(geo_mat.num_volumes()); - bool has_opt_mat{false}; - for (auto impl_id : range(ImplVolumeId{geo_mat.num_volumes()})) + // Construct impl-volume-to-optical mapping + inp.volume_to_mat.assign(geo_mat.num_volumes(), OptMatId{}); + inp.optical_to_core.assign(inp.properties.size(), PhysMatId{}); + + std::unordered_set all_optmat; + for (auto iv_id : range(ImplVolumeId{geo_mat.num_volumes()})) { - OptMatId optmat; - if (PhysMatId matid = geo_mat.material_id(impl_id)) + if (PhysMatId matid = geo_mat.material_id(iv_id)) { auto mat_view = mat.get(matid); - optmat = mat_view.optical_material_id(); + OptMatId optmat = mat_view.optical_material_id(); if (optmat) { - has_opt_mat = true; + CELER_ASSERT(optmat < inp.optical_to_core.size()); + all_optmat.insert(optmat); + inp.optical_to_core[optmat.get()] = matid; + inp.volume_to_mat[iv_id.get()] = optmat; } } - inp.volume_to_mat.push_back(optmat); } - - CELER_VALIDATE(has_opt_mat, + CELER_VALIDATE(!all_optmat.empty(), << "no volumes have associated optical materials"); - CELER_ENSURE(inp.volume_to_mat.size() == geo_mat.num_volumes()); - - // Construct optical to core material mapping - inp.optical_to_core - = std::vector(inp.properties.size(), PhysMatId{}); - for (auto core_id : range(PhysMatId{mat.num_materials()})) - { - if (auto opt_mat_id = mat.get(core_id).optical_material_id()) - { - CELER_EXPECT(opt_mat_id < inp.optical_to_core.size()); - CELER_EXPECT(!inp.optical_to_core[opt_mat_id.get()]); - inp.optical_to_core[opt_mat_id.get()] = core_id; - } - } - CELER_ENSURE(std::all_of( - inp.optical_to_core.begin(), inp.optical_to_core.end(), Identity{})); + CELER_LOG(info) << "Constructed " << inp.properties.size() + << " optical materials with " << all_optmat.size() + << " present in the geometry"; return std::make_shared(std::move(inp)); } From 9ed1cfaa8013b6c52edfc90a40467cbbcb53b6d9 Mon Sep 17 00:00:00 2001 From: Seth R Johnson Date: Mon, 12 Jan 2026 12:30:11 -0500 Subject: [PATCH 3/3] Allow zero cross section for vacuum --- src/celeritas/optical/PhysicsStepUtils.hh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/celeritas/optical/PhysicsStepUtils.hh b/src/celeritas/optical/PhysicsStepUtils.hh index 47807c6c84..732cb5fac3 100644 --- a/src/celeritas/optical/PhysicsStepUtils.hh +++ b/src/celeritas/optical/PhysicsStepUtils.hh @@ -33,13 +33,17 @@ inline CELER_FUNCTION StepLimit calc_physics_step_limit( total_xs += physics.calc_xs(model, particle.energy()); } physics.macro_xs(total_xs); - - CELER_ASSERT(physics.macro_xs() > 0); + CELER_ASSERT(physics.macro_xs() >= 0); StepLimit limit; limit.action = physics.discrete_action(); limit.step = physics.interaction_mfp() / total_xs; + if (CELER_UNLIKELY(total_xs == 0)) + { + limit.action = {}; + } + return limit; }