From 4648f819a8660e1965c2991364a128407331eac2 Mon Sep 17 00:00:00 2001 From: Hayden Hollenbeck Date: Tue, 27 Jan 2026 06:15:40 -0600 Subject: [PATCH 01/11] Initial draft of optical detector callback --- src/celeritas/CMakeLists.txt | 1 + src/celeritas/inp/Problem.hh | 2 + src/celeritas/inp/Scoring.hh | 22 ++++ src/celeritas/optical/CoreParams.cc | 1 + src/celeritas/optical/CoreParams.hh | 10 +- .../optical/detector/DetectorAction.cc | 14 +++ .../optical/detector/DetectorAction.cu | 14 +++ .../optical/detector/DetectorAction.hh | 42 +++++++ .../optical/detector/DetectorData.cc | 32 +++++ .../optical/detector/DetectorData.cu | 37 ++++++ .../optical/detector/DetectorData.hh | 85 +++++++++++++ .../optical/detector/DetectorExecutor.hh | 53 ++++++++ .../optical/detector/ScoringParams.cc | 34 +++++ .../optical/detector/ScoringParams.hh | 41 ++++++ src/celeritas/setup/Problem.cc | 2 + src/celeritas/setup/StandaloneInput.cc | 10 +- test/celeritas/CMakeLists.txt | 1 + test/celeritas/optical/Detector.test.cc | 117 ++++++++++++++++++ test/geocel/data/optical-box.gdml | 44 ++++++- 19 files changed, 557 insertions(+), 5 deletions(-) create mode 100644 src/celeritas/optical/detector/DetectorAction.cc create mode 100644 src/celeritas/optical/detector/DetectorAction.cu create mode 100644 src/celeritas/optical/detector/DetectorAction.hh create mode 100644 src/celeritas/optical/detector/DetectorData.cc create mode 100644 src/celeritas/optical/detector/DetectorData.cu create mode 100644 src/celeritas/optical/detector/DetectorData.hh create mode 100644 src/celeritas/optical/detector/DetectorExecutor.hh create mode 100644 src/celeritas/optical/detector/ScoringParams.cc create mode 100644 src/celeritas/optical/detector/ScoringParams.hh create mode 100644 test/celeritas/optical/Detector.test.cc diff --git a/src/celeritas/CMakeLists.txt b/src/celeritas/CMakeLists.txt index 555e15cc39..0b1777f713 100644 --- a/src/celeritas/CMakeLists.txt +++ b/src/celeritas/CMakeLists.txt @@ -109,6 +109,7 @@ list(APPEND SOURCES optical/CoreParams.cc optical/CoreState.cc optical/CoreTrackData.cc + optical/detector/ScoringParams.cc optical/gen/CherenkovParams.cc optical/gen/ScintillationParams.cc optical/gen/GeneratorBase.cc diff --git a/src/celeritas/inp/Problem.hh b/src/celeritas/inp/Problem.hh index 387443d4b1..139def62d4 100644 --- a/src/celeritas/inp/Problem.hh +++ b/src/celeritas/inp/Problem.hh @@ -92,6 +92,8 @@ struct OpticalProblem OpticalTrackingLimits limits; //! Per-process state sizes for optical tracking loop OpticalStateCapacity capacity; + //! User scoring configuration for optical detectors + OpticalScoring scoring; //! Number of streams size_type num_streams{}; //! Random number generator seed diff --git a/src/celeritas/inp/Scoring.hh b/src/celeritas/inp/Scoring.hh index 8eb49676cd..fea4380ab9 100644 --- a/src/celeritas/inp/Scoring.hh +++ b/src/celeritas/inp/Scoring.hh @@ -6,6 +6,7 @@ //---------------------------------------------------------------------------// #pragma once +#include #include #include #include @@ -19,6 +20,11 @@ class G4LogicalVolume; namespace celeritas { +namespace optical +{ +struct DetectorHit; +} + namespace inp { //---------------------------------------------------------------------------// @@ -148,6 +154,22 @@ struct Scoring std::optional simple_calo; }; +//---------------------------------------------------------------------------// +/*! + * Enable detector callback for hits in optical physics simulations. + */ +struct OpticalScoring +{ + //!@{ + //! \name Type aliases + using HitCallbackFunc + = std::function const&)>; + //!@} + + //! Hit callback function for optical detectors + std::optional detector_callback; +}; + //---------------------------------------------------------------------------// } // namespace inp } // namespace celeritas diff --git a/src/celeritas/optical/CoreParams.cc b/src/celeritas/optical/CoreParams.cc index a6b83e4393..b2aa6f4df8 100644 --- a/src/celeritas/optical/CoreParams.cc +++ b/src/celeritas/optical/CoreParams.cc @@ -126,6 +126,7 @@ CoreParams::CoreParams(Input&& input) : input_(std::move(input)) CP_VALIDATE_INPUT(surface); CP_VALIDATE_INPUT(surface_physics); CP_VALIDATE_INPUT(detectors); + CP_VALIDATE_INPUT(scoring); CP_VALIDATE_INPUT(action_reg); CP_VALIDATE_INPUT(gen_reg); CP_VALIDATE_INPUT(max_streams); diff --git a/src/celeritas/optical/CoreParams.hh b/src/celeritas/optical/CoreParams.hh index 4fd532175c..27cd95bebb 100644 --- a/src/celeritas/optical/CoreParams.hh +++ b/src/celeritas/optical/CoreParams.hh @@ -33,6 +33,7 @@ namespace optical //---------------------------------------------------------------------------// class MaterialParams; class PhysicsParams; +class ScoringParams; class SimParams; class SurfacePhysicsParams; //---------------------------------------------------------------------------// @@ -57,6 +58,7 @@ class CoreParams final : public ParamsDataInterface using SPConstSurface = std::shared_ptr; using SPConstSurfacePhysics = std::shared_ptr; using SPConstDetectors = std::shared_ptr; + using SPConstScoring = std::shared_ptr; using SPConstCherenkov = std::shared_ptr; using SPConstScintillation = std::shared_ptr; @@ -84,6 +86,7 @@ class CoreParams final : public ParamsDataInterface SPConstSurface surface; SPConstSurfacePhysics surface_physics; SPConstDetectors detectors; + SPConstScoring scoring; SPConstCherenkov cherenkov; //!< Optional SPConstScintillation scintillation; //!< Optional @@ -98,9 +101,9 @@ class CoreParams final : public ParamsDataInterface explicit operator bool() const { return geometry && material && rng && sim && surface - && surface_physics && action_reg && gen_reg && max_streams - && capacity.generators > 0 && capacity.tracks > 0 - && capacity.primaries > 0; + && surface_physics && scoring && action_reg && gen_reg + && max_streams && capacity.generators > 0 + && capacity.tracks > 0 && capacity.primaries > 0; } }; @@ -134,6 +137,7 @@ class CoreParams final : public ParamsDataInterface SPAuxRegistry const& aux_reg() const { return input_.aux_reg; } SPGeneratorRegistry const& gen_reg() const { return input_.gen_reg; } SPConstDetectors const& detectors() const { return input_.detectors; } + SPConstScoring const& scoring() const { return input_.scoring; } SPConstCherenkov const& cherenkov() const { return input_.cherenkov; } SPConstScintillation const& scintillation() const { diff --git a/src/celeritas/optical/detector/DetectorAction.cc b/src/celeritas/optical/detector/DetectorAction.cc new file mode 100644 index 0000000000..e8738c44ee --- /dev/null +++ b/src/celeritas/optical/detector/DetectorAction.cc @@ -0,0 +1,14 @@ +//------------------------------- -*- C++ -*- -------------------------------// +// Copyright Celeritas contributors: see top-level COPYRIGHT file for details +// SPDX-License-Identifier: (Apache-2.0 OR MIT) +//---------------------------------------------------------------------------// +//! \file celeritas/optical/detector/DetectorAction.cc +//---------------------------------------------------------------------------// +#include "DetectorAction.hh" + +namespace celeritas +{ +//---------------------------------------------------------------------------// + +//---------------------------------------------------------------------------// +} // namespace celeritas diff --git a/src/celeritas/optical/detector/DetectorAction.cu b/src/celeritas/optical/detector/DetectorAction.cu new file mode 100644 index 0000000000..6a6d8b6eaa --- /dev/null +++ b/src/celeritas/optical/detector/DetectorAction.cu @@ -0,0 +1,14 @@ +//------------------------------ -*- cuda -*- -------------------------------// +// Copyright Celeritas contributors: see top-level COPYRIGHT file for details +// SPDX-License-Identifier: (Apache-2.0 OR MIT) +//---------------------------------------------------------------------------// +//! \file celeritas/optical/detector/DetectorAction.cu +//---------------------------------------------------------------------------// +#include "DetectorAction.hh" + +namespace celeritas +{ +//---------------------------------------------------------------------------// + +//---------------------------------------------------------------------------// +} // namespace celeritas diff --git a/src/celeritas/optical/detector/DetectorAction.hh b/src/celeritas/optical/detector/DetectorAction.hh new file mode 100644 index 0000000000..6f2adbc490 --- /dev/null +++ b/src/celeritas/optical/detector/DetectorAction.hh @@ -0,0 +1,42 @@ +//------------------------------- -*- C++ -*- -------------------------------// +// Copyright Celeritas contributors: see top-level COPYRIGHT file for details +// SPDX-License-Identifier: (Apache-2.0 OR MIT) +//---------------------------------------------------------------------------// +//! \file celeritas/optical/detector/DetectorAction.hh +//---------------------------------------------------------------------------// +#pragma once + +namespace celeritas +{ +//---------------------------------------------------------------------------// +/*! + * Brief class description. + * + * Optional detailed class description, and possibly example usage: + * \code + DetectorAction ...; + \endcode + */ +class DetectorAction +{ + public: + //!@{ + //! \name Type aliases + <++> + //!@} + + public : + // Construct with defaults + inline DetectorAction(); +}; + +//---------------------------------------------------------------------------// +// INLINE DEFINITIONS +//---------------------------------------------------------------------------// +/*! + * Construct with defaults. + */ +DetectorAction::DetectorAction() {} + +//---------------------------------------------------------------------------// +} // namespace celeritas diff --git a/src/celeritas/optical/detector/DetectorData.cc b/src/celeritas/optical/detector/DetectorData.cc new file mode 100644 index 0000000000..c40325aa32 --- /dev/null +++ b/src/celeritas/optical/detector/DetectorData.cc @@ -0,0 +1,32 @@ +//------------------------------- -*- C++ -*- -------------------------------// +// Copyright Celeritas contributors: see top-level COPYRIGHT file for details +// SPDX-License-Identifier: (Apache-2.0 OR MIT) +//---------------------------------------------------------------------------// +//! \file celeritas/optical/detector/DetectorData.cc +//---------------------------------------------------------------------------// +#include "DetectorData.hh" + +namespace celeritas +{ +namespace optical +{ +//---------------------------------------------------------------------------// + +template<> +void copy_hits( + DetectorHitOutput* output, + DetectorStateData const& state) +{ + // Trivial copy to pinned memory + output->hits.reserve(state.all_track_hits.size()); + + for (auto tid : range(TrackSlotId{state.all_track_hits.size()})) + { + output->hits[tid.unchecked_get()] + = state.all_track_hits[tid.unchecked_get()]; + } +} + +//---------------------------------------------------------------------------// +} // namespace optical +} // namespace celeritas diff --git a/src/celeritas/optical/detector/DetectorData.cu b/src/celeritas/optical/detector/DetectorData.cu new file mode 100644 index 0000000000..7d56a40f36 --- /dev/null +++ b/src/celeritas/optical/detector/DetectorData.cu @@ -0,0 +1,37 @@ +//------------------------------ -*- cuda -*- -------------------------------// +// Copyright Celeritas contributors: see top-level COPYRIGHT file for details +// SPDX-License-Identifier: (Apache-2.0 OR MIT) +//---------------------------------------------------------------------------// +//! \file celeritas/optical/detector/DetectorData.cu +//---------------------------------------------------------------------------// +#include "DetectorData.hh" + +namespace celeritas +{ +namespace optical +{ +//---------------------------------------------------------------------------// +template<> +copy_hits(DetectorHitOutput* output, + DetectorStateData const& state) +{ + CELER_EXPECT(output); + + // Trivially copy all track hits from device + + size_type num_tracks = state.all_track_hits.size(); + + output->hits.resize(num_tracks); + Copier copy{{output->hits.data(), num_tracks}, + state.stream_id}; + copy(MemSpace::device, {state.all_track_hits.data().get(), num_tracks}); + + CELER_DEVICE_API_CALL( + StreamSynchronize(celeritas::device().stream(state.stream_id).get())); + + CELER_ENSURE(output->hits.size() == num_tracks); +} + +//---------------------------------------------------------------------------// +} // namespace optical +} // namespace celeritas diff --git a/src/celeritas/optical/detector/DetectorData.hh b/src/celeritas/optical/detector/DetectorData.hh new file mode 100644 index 0000000000..4051d43bab --- /dev/null +++ b/src/celeritas/optical/detector/DetectorData.hh @@ -0,0 +1,85 @@ +//------------------------------- -*- C++ -*- -------------------------------// +// Copyright Celeritas contributors: see top-level COPYRIGHT file for details +// SPDX-License-Identifier: (Apache-2.0 OR MIT) +//---------------------------------------------------------------------------// +//! \file celeritas/optical/detector/DetectorData.hh +//---------------------------------------------------------------------------// +#pragma once + +#include + +#include "corecel/data/PinnedAllocator.hh" +#include "celeritas/Quantities.hh" +#include "celeritas/optical/Types.hh" + +namespace celeritas +{ +namespace optical +{ +//---------------------------------------------------------------------------// +/*! + */ +struct DetectorHit +{ + using Energy = units::MevEnergy; + + DetectorId detector{}; + Energy energy; + real_type time; + Real3 position; + VolumeInstanceId volume_instance; +}; + +// //---------------------------------------------------------------------------// +// /*! +// */ +// struct DetectorHitOutput +// { +// template +// using PinnedVec = std::vector>; +// +// PinnedVec hits; +// }; +// +// //---------------------------------------------------------------------------// +// /*! +// */ +// template +// struct DetectorStateData +// { +// template +// using Items = StateCollection; +// +// StreamId stream_id{}; +// Items all_track_hits; +// }; +// +// //---------------------------------------------------------------------------// +// +// template +// void copy_hits(DetectorHitOutput* output, +// DetectorStateData const& state); +// +// template<> +// void copy_hits(DetectorHitOutput* output, +// DetectorStateData +// const&); +// +// template<> +// void copy_hits(DetectorHitOutput* output, +// DetectorStateData +// const&); +// +// #if !CELER_USE_DEVICE +// template<> +// inline void +// copy_hits(DetectorHitOutput* output, +// DetectorStateData const&) +// { +// CELER_NOT_CONFIGURED("CUDA OR HIP"); +// } +// #endif + +//---------------------------------------------------------------------------// +} // namespace optical +} // namespace celeritas diff --git a/src/celeritas/optical/detector/DetectorExecutor.hh b/src/celeritas/optical/detector/DetectorExecutor.hh new file mode 100644 index 0000000000..b3db11c6c1 --- /dev/null +++ b/src/celeritas/optical/detector/DetectorExecutor.hh @@ -0,0 +1,53 @@ +//------------------------------- -*- C++ -*- -------------------------------// +// Copyright Celeritas contributors: see top-level COPYRIGHT file for details +// SPDX-License-Identifier: (Apache-2.0 OR MIT) +//---------------------------------------------------------------------------// +//! \file celeritas/optical/detector/DetectorExecutor.hh +//---------------------------------------------------------------------------// +#pragma once + +namespace celeritas +{ +namespace optical +{ +//---------------------------------------------------------------------------// +/*! + */ +struct DetectorExecutor +{ + NativeRef data; + + inline CELER_FUNCTION void operator()(CoreTrackView const&) const; +}; + +//---------------------------------------------------------------------------// +// INLINE DEFINITIONS +//---------------------------------------------------------------------------// +/*! + */ +CELER_FUNCTION void +DetectorExecutor::operator()(CoreTrackView const& track) const +{ + auto const detectors = track.detectors(); + + auto const volume_id = track.geometry().volume_id(); + auto const detector_id = detectors.detector_id(volume_id); + + DetectorHit& hit = data.all_track_hits[track.track_slot_id()]; + hit.detector = detector_id; + + if (detector_id) + { + // Populate hit data + hit.energy = track.particle().energy(); + hit.time = track.sim().time(); + hit.position = track.geometry().pos(); + hit.volume_instance = track.geometry().volume_instance_id(); + + // Kill the track + } +} + +//---------------------------------------------------------------------------// +} // namespace optical +} // namespace celeritas diff --git a/src/celeritas/optical/detector/ScoringParams.cc b/src/celeritas/optical/detector/ScoringParams.cc new file mode 100644 index 0000000000..335e7f3b27 --- /dev/null +++ b/src/celeritas/optical/detector/ScoringParams.cc @@ -0,0 +1,34 @@ +//------------------------------- -*- C++ -*- -------------------------------// +// Copyright Celeritas contributors: see top-level COPYRIGHT file for details +// SPDX-License-Identifier: (Apache-2.0 OR MIT) +//---------------------------------------------------------------------------// +//! \file celeritas/optical/detector/ScoringParams.cc +//---------------------------------------------------------------------------// +#include "ScoringParams.hh" + +#include "corecel/cont/Span.hh" +#include "corecel/io/Logger.hh" + +#include "DetectorData.hh" + +namespace celeritas +{ +namespace optical +{ +//---------------------------------------------------------------------------// +ScoringParams::ScoringParams(inp::OpticalScoring input) + : detector_callback_(std::move(input.detector_callback)) +{ + if (detector_callback_) + { + CELER_LOG(info) << "optical scoring enabled."; + } + else + { + CELER_LOG(info) << "optical scoring disabled."; + } +} + +//---------------------------------------------------------------------------// +} // namespace optical +} // namespace celeritas diff --git a/src/celeritas/optical/detector/ScoringParams.hh b/src/celeritas/optical/detector/ScoringParams.hh new file mode 100644 index 0000000000..6dc3f6abc1 --- /dev/null +++ b/src/celeritas/optical/detector/ScoringParams.hh @@ -0,0 +1,41 @@ +//------------------------------- -*- C++ -*- -------------------------------// +// Copyright Celeritas contributors: see top-level COPYRIGHT file for details +// SPDX-License-Identifier: (Apache-2.0 OR MIT) +//---------------------------------------------------------------------------// +//! \file celeritas/optical/detector/ScoringParams.hh +//---------------------------------------------------------------------------// +#pragma once + +#include "celeritas/inp/Scoring.hh" + +namespace celeritas +{ +namespace optical +{ +//---------------------------------------------------------------------------// +/*! + * Brief class description. + * + * Optional detailed class description, and possibly example usage: + * \code + ScoringParams ...; + \endcode + */ +class ScoringParams +{ + public: + //!@{ + //! \name Type aliases + using HitCallbackFunc = inp::OpticalScoring::HitCallbackFunc; + //!@} + + public: + ScoringParams(inp::OpticalScoring); + + private: + std::optional detector_callback_; +}; + +//---------------------------------------------------------------------------// +} // namespace optical +} // namespace celeritas diff --git a/src/celeritas/setup/Problem.cc b/src/celeritas/setup/Problem.cc index d1893d0549..16a0c3b135 100644 --- a/src/celeritas/setup/Problem.cc +++ b/src/celeritas/setup/Problem.cc @@ -73,6 +73,7 @@ #include "celeritas/optical/PhysicsParams.hh" #include "celeritas/optical/SimParams.hh" #include "celeritas/optical/Transporter.hh" +#include "celeritas/optical/detector/ScoringParams.hh" #include "celeritas/optical/gen/CherenkovParams.hh" #include "celeritas/optical/gen/DirectGeneratorAction.hh" #include "celeritas/optical/gen/GeneratorAction.hh" @@ -389,6 +390,7 @@ auto build_optical_params(inp::OpticalProblem const& p, pi.surface_physics = std::make_shared( pi.action_reg.get(), p.physics.surfaces); pi.detectors = std::move(loaded_model.detector); + pi.scoring = std::make_shared(p.scoring); // Streams and capacities pi.max_streams = p.num_streams; diff --git a/src/celeritas/setup/StandaloneInput.cc b/src/celeritas/setup/StandaloneInput.cc index fa7ebda0ff..5c6c91dfa6 100644 --- a/src/celeritas/setup/StandaloneInput.cc +++ b/src/celeritas/setup/StandaloneInput.cc @@ -138,7 +138,15 @@ OpticalStandaloneLoaded standalone_input(inp::OpticalStandaloneInput& si) // Load geometry, surfaces, regions from Geant4 world pointer CELER_ASSERT(geant_setup.geo_params()); - si.problem.model = geant_setup.geo_params()->make_model_input(); + { + // TODO: have a better way to define detectors for standalone optical + // simulations. + CELER_LOG(warning) << "Sensitive detectors from GDML not currently " + "supported! Please be careful!"; + auto dets = si.problem.model.detectors; + si.problem.model = geant_setup.geo_params()->make_model_input(); + si.problem.model.detectors = std::move(dets); + } // Import optical physics data from Geant4 ImportData imported; diff --git a/test/celeritas/CMakeLists.txt b/test/celeritas/CMakeLists.txt index 44a143a1e2..70a363d4ab 100644 --- a/test/celeritas/CMakeLists.txt +++ b/test/celeritas/CMakeLists.txt @@ -442,6 +442,7 @@ endfunction() celeritas_add_standalone_tests(optical/Generator LArSphereGeneratorTest primary) celeritas_add_standalone_tests(optical/Generator LArSphereGeneratorTest direct) celeritas_add_standalone_tests(optical/Generator LArSphereGeneratorTest offload) +celeritas_add_standalone_tests(optical/Detector DetectorTest simple) celeritas_add_test(optical/Absorption.test.cc) celeritas_add_test(optical/Cherenkov.test.cc) diff --git a/test/celeritas/optical/Detector.test.cc b/test/celeritas/optical/Detector.test.cc new file mode 100644 index 0000000000..5d06fd28d7 --- /dev/null +++ b/test/celeritas/optical/Detector.test.cc @@ -0,0 +1,117 @@ +//------------------------------- -*- C++ -*- -------------------------------// +// Copyright Celeritas contributors: see top-level COPYRIGHT file for details +// SPDX-License-Identifier: (Apache-2.0 OR MIT) +//---------------------------------------------------------------------------// +//! \file celeritas/optical/Detector.test.cc +//---------------------------------------------------------------------------// +#include +#include + +#include "geocel/UnitUtils.hh" +#include "celeritas/inp/StandaloneInput.hh" +#include "celeritas/optical/Runner.hh" +#include "celeritas/optical/Types.hh" + +#include "celeritas_test.hh" + +namespace celeritas +{ +namespace test +{ +//---------------------------------------------------------------------------// +class DetectorTest : public Test +{ + public: + void SetUp() override + { + osi_.problem.model.geometry + = Test::test_data_path("geocel", "optical-box.gdml"); + + osi_.problem.generator = inp::OpticalDirectGenerator{}; + osi_.problem.capacity = [] { + inp::OpticalStateCapacity cap; + cap.tracks = 32; + cap.primaries = 8 * cap.tracks; + cap.generators = 2 * cap.tracks; + return cap; + }(); + + osi_.problem.num_streams = 1; + + osi_.geant_setup = GeantOpticalPhysicsOptions::deactivated(); + osi_.geant_setup.absorption = true; + + osi_.problem.physics.surfaces = [] { + inp::SurfacePhysics input; + + // Center-top surface is only absorption + + PhysSurfaceId phys_surface{0}; + input.materials.push_back({}); + input.roughness.polished.emplace(phys_surface, inp::NoRoughness{}); + input.reflectivity.fresnel.emplace(phys_surface, + inp::FresnelReflection{}); + input.interaction.trivial.emplace( + phys_surface, optical::TrivialInteractionMode::absorb); + + // Default surface is transmission + + phys_surface++; + input.materials.push_back({}); + input.roughness.polished.emplace(phys_surface, inp::NoRoughness{}); + input.reflectivity.fresnel.emplace(phys_surface, + inp::FresnelReflection{}); + input.interaction.trivial.emplace( + phys_surface, optical::TrivialInteractionMode::transmit); + + return input; + }(); + + osi_.problem.model.detectors.detectors = { + {"y-detectors", {VolumeId{2}}}, + {"x-detectors", {VolumeId{3}, VolumeId{4}}}, + {"z-detectors", {VolumeId{5}, VolumeId{6}}}, + }; + } + + protected: + inp::OpticalStandaloneInput osi_; +}; + +/* + * - Figure out how to add detectors in setup + * - Write a test to check individual photon hit results + * - Write a test to check bulk photon hits + * - Write a test to check bulk photon hits on device + */ + +TEST_F(DetectorTest, simple) +{ + size_type num_tracks = osi_.problem.capacity.tracks * 4; + + std::vector const inits( + num_tracks, + optical::TrackInitializer{units::MevEnergy{3e-6}, + from_cm(Real3{0, 49, 0}), + Real3{0, -1, 0}, // direction + Real3{0, 0, 1}, // polarization + 0, + {}, // primary + ImplVolumeId{0}}); + + auto result = optical::Runner(std::move(osi_))(make_span(inits)); + + EXPECT_EQ(0, result.counters.steps); + EXPECT_EQ(0, result.counters.step_iters); + EXPECT_EQ(1, result.counters.flushes); + ASSERT_EQ(1, result.counters.generators.size()); + + auto const& gen = result.counters.generators.front(); + EXPECT_EQ(num_tracks, gen.buffer_size); + EXPECT_EQ(0, gen.num_pending); + EXPECT_EQ(num_tracks, gen.num_generated); +} + +//---------------------------------------------------------------------------// +} // namespace test +} // namespace celeritas diff --git a/test/geocel/data/optical-box.gdml b/test/geocel/data/optical-box.gdml index 3a1889b5b0..94e7085c25 100644 --- a/test/geocel/data/optical-box.gdml +++ b/test/geocel/data/optical-box.gdml @@ -44,11 +44,33 @@ - + + + + + + + + + + + + + + + + + + + + + + + @@ -60,6 +82,26 @@ + + + + + + + + + + + + + + + + + + + + From 62300bc84815f87383c69d473dc96452ddc11232 Mon Sep 17 00:00:00 2001 From: Hayden Hollenbeck Date: Tue, 27 Jan 2026 07:50:30 -0600 Subject: [PATCH 02/11] Simple detector test --- src/celeritas/optical/CoreParams.cc | 1 - src/celeritas/optical/CoreParams.hh | 8 +- test/celeritas/optical/Detector.test.cc | 113 +++++++++++++++++++++++- 3 files changed, 115 insertions(+), 7 deletions(-) diff --git a/src/celeritas/optical/CoreParams.cc b/src/celeritas/optical/CoreParams.cc index b2aa6f4df8..a6b83e4393 100644 --- a/src/celeritas/optical/CoreParams.cc +++ b/src/celeritas/optical/CoreParams.cc @@ -126,7 +126,6 @@ CoreParams::CoreParams(Input&& input) : input_(std::move(input)) CP_VALIDATE_INPUT(surface); CP_VALIDATE_INPUT(surface_physics); CP_VALIDATE_INPUT(detectors); - CP_VALIDATE_INPUT(scoring); CP_VALIDATE_INPUT(action_reg); CP_VALIDATE_INPUT(gen_reg); CP_VALIDATE_INPUT(max_streams); diff --git a/src/celeritas/optical/CoreParams.hh b/src/celeritas/optical/CoreParams.hh index 27cd95bebb..9fcf5d66a7 100644 --- a/src/celeritas/optical/CoreParams.hh +++ b/src/celeritas/optical/CoreParams.hh @@ -86,8 +86,8 @@ class CoreParams final : public ParamsDataInterface SPConstSurface surface; SPConstSurfacePhysics surface_physics; SPConstDetectors detectors; - SPConstScoring scoring; + SPConstScoring scoring; //!< Optional SPConstCherenkov cherenkov; //!< Optional SPConstScintillation scintillation; //!< Optional @@ -101,9 +101,9 @@ class CoreParams final : public ParamsDataInterface explicit operator bool() const { return geometry && material && rng && sim && surface - && surface_physics && scoring && action_reg && gen_reg - && max_streams && capacity.generators > 0 - && capacity.tracks > 0 && capacity.primaries > 0; + && surface_physics && action_reg && gen_reg && max_streams + && capacity.generators > 0 && capacity.tracks > 0 + && capacity.primaries > 0; } }; diff --git a/test/celeritas/optical/Detector.test.cc b/test/celeritas/optical/Detector.test.cc index 5d06fd28d7..e976ee8cad 100644 --- a/test/celeritas/optical/Detector.test.cc +++ b/test/celeritas/optical/Detector.test.cc @@ -11,6 +11,7 @@ #include "celeritas/inp/StandaloneInput.hh" #include "celeritas/optical/Runner.hh" #include "celeritas/optical/Types.hh" +#include "celeritas/optical/detector/DetectorData.hh" #include "celeritas_test.hh" @@ -79,13 +80,121 @@ class DetectorTest : public Test }; /* - * - Figure out how to add detectors in setup - * - Write a test to check individual photon hit results * - Write a test to check bulk photon hits * - Write a test to check bulk photon hits on device */ +struct SimpleScorer +{ + std::vector detector_ids; + std::vector energies; + std::vector times; + std::vector x_positions; + std::vector y_positions; + std::vector z_positions; + std::vector volume_instance_ids; + + void operator()(Span const& new_hits) + { + for (auto const& hit : new_hits) + { + detector_ids.push_back(hit.detector.get()); + energies.push_back(value_as(hit.energy)); + times.push_back(hit.time); + x_positions.push_back(hit.position[0]); + y_positions.push_back(hit.position[1]); + z_positions.push_back(hit.position[2]); + volume_instance_ids.push_back(hit.volume_instance.get()); + } + } +}; + TEST_F(DetectorTest, simple) +{ + SimpleScorer scores; + osi_.problem.scoring.detector_callback = scores; + + using E = units::MevEnergy; + using TI = optical::TrackInitializer; + + std::vector const inits{ + TI{E{1e-6}, + Real3{0, 0, 0}, // pos + Real3{1, 0, 0}, // dir + Real3{0, 1, 0}, // pol + 0, // time + {}, + ImplVolumeId{0}}, + TI{E{2e-6}, + Real3{0, 0, 0}, // pos + Real3{-1, 0, 0}, // dir + Real3{0, 1, 0}, // pol + 10, // time + {}, + ImplVolumeId{0}}, + TI{E{3e-6}, + Real3{0, 0, 0}, // pos + Real3{0, 0, 1}, // dir + Real3{0, 1, 0}, // pol + 1, // time + {}, + ImplVolumeId{0}}, + TI{E{4e-6}, + Real3{0, 0, 0}, // pos + Real3{0, 0, -1}, // dir + Real3{0, 1, 0}, // pol + 20, // time + {}, + ImplVolumeId{0}}, + TI{E{5e-6}, + Real3{0, 0, 0}, // pos + Real3{1, 0, 0}, // dir + Real3{0, 1, 0}, // pol + 13, // time + {}, + ImplVolumeId{0}}, + TI{E{2e-6}, + Real3{0, 0, 0}, // pos + Real3{0, 1, 0}, // dir + Real3{1, 0, 0}, // pol + 2, // time + {}, + ImplVolumeId{0}}, + TI{E{6e-6}, + Real3{0, 0, 0}, // pos + Real3{0, -1, 0}, // dir + Real3{1, 0, 0}, // pol + 7, // time + {}, + ImplVolumeId{0}}, + }; + + auto result = optical::Runner(std::move(osi_))(make_span(inits)); + + EXPECT_EQ(0, result.counters.steps); + EXPECT_EQ(0, result.counters.step_iters); + EXPECT_EQ(1, result.counters.flushes); + ASSERT_EQ(1, result.counters.generators.size()); + + static size_type const expected_detector_ids[] = {1, 1, 2, 2, 1, 0}; + static real_type const expected_energies[] + = {1e-6, 2e-6, 3e-6, 4e-6, 5e-6, 6e-6}; + static real_type const expected_x_positions[] = {0}; + static real_type const expected_y_positions[] = {0}; + static real_type const expected_z_positions[] = {0}; + static real_type const expected_times[] = {0}; + static size_type const expected_volume_instance_ids[] = {3, 4, 5, 6, 3, 2}; + + EXPECT_VEC_EQ(expected_detector_ids, scores.detector_ids); + EXPECT_VEC_SOFT_EQ(expected_energies, scores.energies); + EXPECT_VEC_SOFT_EQ(expected_x_positions, scores.x_positions); + EXPECT_VEC_SOFT_EQ(expected_y_positions, scores.y_positions); + EXPECT_VEC_SOFT_EQ(expected_z_positions, scores.z_positions); + EXPECT_VEC_SOFT_EQ(expected_times, scores.times); + EXPECT_VEC_EQ(expected_volume_instance_ids, scores.volume_instance_ids); +} + +TEST_F(DetectorTest, stress) { size_type num_tracks = osi_.problem.capacity.tracks * 4; From 06f831bbe1d03f347a12b348361a4e17cb7a11ae Mon Sep 17 00:00:00 2001 From: Hayden Hollenbeck Date: Tue, 27 Jan 2026 13:58:17 -0600 Subject: [PATCH 03/11] Pass first test --- src/celeritas/CMakeLists.txt | 2 + src/celeritas/optical/CoreTrackData.cc | 1 + src/celeritas/optical/CoreTrackData.hh | 7 +- src/celeritas/optical/CoreTrackView.hh | 15 ++ .../optical/detector/DetectorAction.cc | 35 +++++ .../optical/detector/DetectorAction.cu | 21 +++ .../optical/detector/DetectorAction.hh | 64 +++++++-- .../optical/detector/DetectorData.cc | 7 +- .../optical/detector/DetectorData.hh | 135 +++++++++++------- .../optical/detector/DetectorExecutor.hh | 43 ++++-- .../optical/detector/ScoringParams.cc | 24 +++- .../optical/detector/ScoringParams.hh | 18 +-- .../optical/detector/ScoringTrackView.hh | 92 ++++++++++++ src/celeritas/setup/Problem.cc | 3 +- test/celeritas/optical/Detector.test.cc | 82 +++++------ 15 files changed, 412 insertions(+), 137 deletions(-) create mode 100644 src/celeritas/optical/detector/ScoringTrackView.hh diff --git a/src/celeritas/CMakeLists.txt b/src/celeritas/CMakeLists.txt index 0b1777f713..005c7343f9 100644 --- a/src/celeritas/CMakeLists.txt +++ b/src/celeritas/CMakeLists.txt @@ -390,6 +390,8 @@ celeritas_polysource(optical/action/DiscreteSelectAction) celeritas_polysource(optical/action/PreStepAction) celeritas_polysource(optical/action/TrackingCutAction) celeritas_polysource(optical/action/detail/TrackInitAlgorithms) +celeritas_polysource(optical/detector/DetectorData) +celeritas_polysource(optical/detector/DetectorAction) celeritas_polysource(optical/gen/GeneratorAction) celeritas_polysource(optical/gen/OffloadAction) celeritas_polysource(optical/gen/OffloadGatherAction) diff --git a/src/celeritas/optical/CoreTrackData.cc b/src/celeritas/optical/CoreTrackData.cc index dbe0c359b9..42d06fb22c 100644 --- a/src/celeritas/optical/CoreTrackData.cc +++ b/src/celeritas/optical/CoreTrackData.cc @@ -37,6 +37,7 @@ void resize(CoreStateData* state, resize(&state->particle, size); resize(&state->physics, size); resize(&state->rng, params.rng, stream_id, size); + resize(&state->scoring, size); resize(&state->sim, size); resize(&state->surface_physics, size); resize(&state->init, stream_id, size); diff --git a/src/celeritas/optical/CoreTrackData.hh b/src/celeritas/optical/CoreTrackData.hh index abbac783c7..ab858dd84c 100644 --- a/src/celeritas/optical/CoreTrackData.hh +++ b/src/celeritas/optical/CoreTrackData.hh @@ -20,6 +20,7 @@ #include "PhysicsData.hh" #include "SimData.hh" #include "TrackInitData.hh" +#include "detector/DetectorData.hh" #include "gen/CherenkovData.hh" #include "gen/ScintillationData.hh" #include "surface/SurfacePhysicsData.hh" @@ -103,6 +104,7 @@ struct CoreStateData ParticleStateData particle; PhysicsStateData physics; RngStateData rng; + DetectorStateData scoring; SimStateData sim; SurfacePhysicsStateData surface_physics; TrackInitStateData init; @@ -116,8 +118,8 @@ struct CoreStateData //! Whether the data are assigned explicit CELER_FUNCTION operator bool() const { - return geometry && particle && physics && rng && sim && surface_physics - && init && stream_id; + return geometry && particle && physics && rng && scoring && sim + && surface_physics && init && stream_id; } //! Assign from another set of data @@ -129,6 +131,7 @@ struct CoreStateData particle = other.particle; physics = other.physics; rng = other.rng; + scoring = other.scoring; sim = other.sim; surface_physics = other.surface_physics; init = other.init; diff --git a/src/celeritas/optical/CoreTrackView.hh b/src/celeritas/optical/CoreTrackView.hh index f585c22d44..a781516e1d 100644 --- a/src/celeritas/optical/CoreTrackView.hh +++ b/src/celeritas/optical/CoreTrackView.hh @@ -17,6 +17,7 @@ #include "PhysicsTrackView.hh" #include "SimTrackView.hh" #include "TrackInitializer.hh" +#include "detector/ScoringTrackView.hh" #include "surface/SurfacePhysicsTrackView.hh" #if !CELER_DEVICE_COMPILE @@ -82,6 +83,9 @@ class CoreTrackView // Return a sensitive detector view inline CELER_FUNCTION DetectorView detectors() const; + // Return a scoring view + inline CELER_FUNCTION ScoringTrackView scoring() const; + // Return an RNG engine inline CELER_FUNCTION RngEngine rng() const; @@ -151,6 +155,9 @@ CoreTrackView::operator=(TrackInitializer const& init) // Initialize the surface state this->surface_physics().reset(); + // Initialize scoring + this->scoring().clear_hit(); + return *this; } @@ -259,6 +266,14 @@ CELER_FUNCTION auto CoreTrackView::detectors() const -> DetectorView return DetectorView{params_.detectors}; } +//---------------------------------------------------------------------------// +/*! + * Return a scoring view. + */ +CELER_FUNCTION auto CoreTrackView::scoring() const -> ScoringTrackView +{ + return ScoringTrackView{states_.scoring, this->track_slot_id()}; +} //---------------------------------------------------------------------------// /*! * Return the RNG engine. diff --git a/src/celeritas/optical/detector/DetectorAction.cc b/src/celeritas/optical/detector/DetectorAction.cc index e8738c44ee..30914f7648 100644 --- a/src/celeritas/optical/detector/DetectorAction.cc +++ b/src/celeritas/optical/detector/DetectorAction.cc @@ -6,9 +6,44 @@ //---------------------------------------------------------------------------// #include "DetectorAction.hh" +#include "celeritas/optical/action/ActionLauncher.hh" +#include "celeritas/optical/action/TrackSlotExecutor.hh" + +#include "DetectorExecutor.hh" + namespace celeritas { +namespace optical +{ +//---------------------------------------------------------------------------// +/*! + * Construct with action ID. + */ +DetectorAction::DetectorAction(ActionId aid) + : StaticConcreteAction(aid, "scoring-detector", "Score detector hits") +{ +} + //---------------------------------------------------------------------------// +/*! + * Launch the detector action on host. + */ +void DetectorAction::step(CoreParams const& params, CoreStateHost& state) const +{ + TrackSlotExecutor execute{ + params.ptr(), state.ptr(), DetectorExecutor{}}; + launch_action(state, execute); + + this->process_hits(params, state); +} + +#if !CELER_USE_DEVICE +void DetectorAction::step(CoreParams const&, CoreStateDevice&) const +{ + CELER_NOT_CONFIGURED("CUDA OR HIP"); +} +#endif //---------------------------------------------------------------------------// +} // namespace optical } // namespace celeritas diff --git a/src/celeritas/optical/detector/DetectorAction.cu b/src/celeritas/optical/detector/DetectorAction.cu index 6a6d8b6eaa..c5b6347806 100644 --- a/src/celeritas/optical/detector/DetectorAction.cu +++ b/src/celeritas/optical/detector/DetectorAction.cu @@ -6,9 +6,30 @@ //---------------------------------------------------------------------------// #include "DetectorAction.hh" +#include "celeritas/optical/action/ActionLauncher.device.hh" +#include "celeritas/optical/action/TrackSlotExecutor.hh" + +#include "DetectorExecutor.hh" + namespace celeritas { +namespace optical +{ //---------------------------------------------------------------------------// +/*! + * Launch the detector action on device. + */ +void DetectorAction::step(CoreParams const& params, CoreStateDevice& state) const +{ + TrackSlotExecutor execute{ + params.ptr(), state.ptr(), DetectorExecutor{}}; + + static ActionLauncher const launch_kernel(*this); + launch_kernel(state, execute); + + this->process_hits(params, state); +} //---------------------------------------------------------------------------// +} // namespace optical } // namespace celeritas diff --git a/src/celeritas/optical/detector/DetectorAction.hh b/src/celeritas/optical/detector/DetectorAction.hh index 6f2adbc490..7dbfd03f2c 100644 --- a/src/celeritas/optical/detector/DetectorAction.hh +++ b/src/celeritas/optical/detector/DetectorAction.hh @@ -6,8 +6,19 @@ //---------------------------------------------------------------------------// #pragma once +#include + +#include "celeritas/optical/CoreParams.hh" +#include "celeritas/optical/CoreState.hh" +#include "celeritas/optical/action/ActionInterface.hh" + +#include "DetectorData.hh" +#include "ScoringParams.hh" + namespace celeritas { +namespace optical +{ //---------------------------------------------------------------------------// /*! * Brief class description. @@ -17,26 +28,63 @@ namespace celeritas DetectorAction ...; \endcode */ -class DetectorAction +class DetectorAction final : public OpticalStepActionInterface, + public StaticConcreteAction { public: //!@{ //! \name Type aliases - <++> - //!@} + //!@} + + public: + // Construct with ID + explicit DetectorAction(ActionId); + + // Launch kernel with host data + void step(CoreParams const&, CoreStateHost&) const final; - public : - // Construct with defaults - inline DetectorAction(); + // Launch kernel with device data + void step(CoreParams const&, CoreStateDevice&) const final; + + //! Dependency ordering of the action + StepActionOrder order() const final { return StepActionOrder::post; } + + private: + template + void process_hits(CoreParams const&, CoreState&) const; }; //---------------------------------------------------------------------------// // INLINE DEFINITIONS //---------------------------------------------------------------------------// /*! - * Construct with defaults. */ -DetectorAction::DetectorAction() {} +template +void DetectorAction::process_hits(CoreParams const& params, + CoreState& state) const +{ + DetectorHitOutput hit_results; + + // Copy hits (possibly from device) into pinned vector + copy_hits(&hit_results, state.ref().scoring); + + // Erase all hits with invalid detector ID + hit_results.hits.erase( + std::remove_if(hit_results.hits.begin(), + hit_results.hits.end(), + [](DetectorHit const& hit) { + return !static_cast(hit.detector); + }), + hit_results.hits.end()); + + if (!hit_results.hits.empty()) + { + auto scoring = params.scoring(); + CELER_ASSERT(scoring); + scoring->process_hits(make_span(hit_results.hits)); + } +} //---------------------------------------------------------------------------// +} // namespace optical } // namespace celeritas diff --git a/src/celeritas/optical/detector/DetectorData.cc b/src/celeritas/optical/detector/DetectorData.cc index c40325aa32..0dccfcbde6 100644 --- a/src/celeritas/optical/detector/DetectorData.cc +++ b/src/celeritas/optical/detector/DetectorData.cc @@ -6,6 +6,8 @@ //---------------------------------------------------------------------------// #include "DetectorData.hh" +#include + namespace celeritas { namespace optical @@ -18,12 +20,11 @@ void copy_hits( DetectorStateData const& state) { // Trivial copy to pinned memory - output->hits.reserve(state.all_track_hits.size()); + output->hits.resize(state.all_track_hits.size()); for (auto tid : range(TrackSlotId{state.all_track_hits.size()})) { - output->hits[tid.unchecked_get()] - = state.all_track_hits[tid.unchecked_get()]; + output->hits[tid.unchecked_get()] = state.all_track_hits[tid]; } } diff --git a/src/celeritas/optical/detector/DetectorData.hh b/src/celeritas/optical/detector/DetectorData.hh index 4051d43bab..53b7f9bfc8 100644 --- a/src/celeritas/optical/detector/DetectorData.hh +++ b/src/celeritas/optical/detector/DetectorData.hh @@ -8,6 +8,7 @@ #include +#include "corecel/data/Collection.hh" #include "corecel/data/PinnedAllocator.hh" #include "celeritas/Quantities.hh" #include "celeritas/optical/Types.hh" @@ -30,55 +31,91 @@ struct DetectorHit VolumeInstanceId volume_instance; }; -// //---------------------------------------------------------------------------// -// /*! -// */ -// struct DetectorHitOutput -// { -// template -// using PinnedVec = std::vector>; -// -// PinnedVec hits; -// }; -// -// //---------------------------------------------------------------------------// -// /*! -// */ -// template -// struct DetectorStateData -// { -// template -// using Items = StateCollection; -// -// StreamId stream_id{}; -// Items all_track_hits; -// }; -// -// //---------------------------------------------------------------------------// -// -// template -// void copy_hits(DetectorHitOutput* output, -// DetectorStateData const& state); -// -// template<> -// void copy_hits(DetectorHitOutput* output, -// DetectorStateData -// const&); -// -// template<> -// void copy_hits(DetectorHitOutput* output, -// DetectorStateData -// const&); -// -// #if !CELER_USE_DEVICE -// template<> -// inline void -// copy_hits(DetectorHitOutput* output, -// DetectorStateData const&) -// { -// CELER_NOT_CONFIGURED("CUDA OR HIP"); -// } -// #endif +//---------------------------------------------------------------------------// +/*! + */ +struct DetectorHitOutput +{ + template + using PinnedVec = std::vector>; + + PinnedVec hits; +}; + +//---------------------------------------------------------------------------// +/*! + */ +template +struct DetectorStateData +{ + //!@{ + //! \name Type aliases + template + using StateItems = StateCollection; + //!@} + + StreamId stream_id{}; + StateItems all_track_hits; + + //! Whether data is assigned and valid + explicit CELER_FUNCTION operator bool() const + { + return !all_track_hits.empty(); + } + + //! State size + CELER_FUNCTION size_type size() const { return all_track_hits.size(); } + + //! Assign from another set of data + template + DetectorStateData& operator=(DetectorStateData& other) + { + CELER_EXPECT(other); + stream_id = other.stream_id; + all_track_hits = other.all_track_hits; + return *this; + } +}; + +//---------------------------------------------------------------------------// +/*! + * Resize the state in host code. + */ +template +inline void +resize(DetectorStateData* state, size_type size) +{ + CELER_EXPECT(state); + CELER_EXPECT(size > 0); + + resize(&state->all_track_hits, size); + + CELER_ENSURE(*state); +} + +//---------------------------------------------------------------------------// + +template +void copy_hits(DetectorHitOutput* output, + DetectorStateData const& state); + +template<> +void copy_hits(DetectorHitOutput* output, + DetectorStateData const&); + +template<> +void copy_hits(DetectorHitOutput* output, + DetectorStateData const&); + +#if !CELER_USE_DEVICE +template<> +inline void +copy_hits(DetectorHitOutput* output, + DetectorStateData const&) +{ + CELER_NOT_CONFIGURED("CUDA OR HIP"); +} +#endif //---------------------------------------------------------------------------// } // namespace optical diff --git a/src/celeritas/optical/detector/DetectorExecutor.hh b/src/celeritas/optical/detector/DetectorExecutor.hh index b3db11c6c1..c1976466de 100644 --- a/src/celeritas/optical/detector/DetectorExecutor.hh +++ b/src/celeritas/optical/detector/DetectorExecutor.hh @@ -15,8 +15,6 @@ namespace optical */ struct DetectorExecutor { - NativeRef data; - inline CELER_FUNCTION void operator()(CoreTrackView const&) const; }; @@ -28,23 +26,38 @@ struct DetectorExecutor CELER_FUNCTION void DetectorExecutor::operator()(CoreTrackView const& track) const { - auto const detectors = track.detectors(); + auto score = track.scoring(); + auto sim = track.sim(); - auto const volume_id = track.geometry().volume_id(); - auto const detector_id = detectors.detector_id(volume_id); + if (sim.status() == TrackStatus::alive) + { + auto const detectors = track.detectors(); - DetectorHit& hit = data.all_track_hits[track.track_slot_id()]; - hit.detector = detector_id; + auto geometry = track.geometry(); - if (detector_id) - { - // Populate hit data - hit.energy = track.particle().energy(); - hit.time = track.sim().time(); - hit.position = track.geometry().pos(); - hit.volume_instance = track.geometry().volume_instance_id(); + auto const volume_id = geometry.volume_id(); + auto const detector_id = detectors.detector_id(volume_id); - // Kill the track + if (detector_id) + { + score.score_hit(DetectorHit{detector_id, + track.particle().energy(), + sim.time(), + geometry.pos(), + geometry.volume_instance_id()}); + + // Kill the track + sim.status(TrackStatus::killed); + } + else + { + score.clear_hit(); + } + } + else + { + // Ensure killed, inactive, and errored tracks don't contribute to hits + score.clear_hit(); } } diff --git a/src/celeritas/optical/detector/ScoringParams.cc b/src/celeritas/optical/detector/ScoringParams.cc index 335e7f3b27..8f8cc841ec 100644 --- a/src/celeritas/optical/detector/ScoringParams.cc +++ b/src/celeritas/optical/detector/ScoringParams.cc @@ -8,7 +8,9 @@ #include "corecel/cont/Span.hh" #include "corecel/io/Logger.hh" +#include "corecel/sys/ActionRegistry.hh" +#include "DetectorAction.hh" #include "DetectorData.hh" namespace celeritas @@ -16,12 +18,22 @@ namespace celeritas namespace optical { //---------------------------------------------------------------------------// -ScoringParams::ScoringParams(inp::OpticalScoring input) +/*! + */ +ScoringParams::ScoringParams(ActionRegistry* action_reg, + inp::OpticalScoring input) : detector_callback_(std::move(input.detector_callback)) { + CELER_EXPECT(action_reg); + if (detector_callback_) { CELER_LOG(info) << "optical scoring enabled."; + + detector_action_ + = std::make_shared(action_reg->next_id()); + CELER_ASSERT(detector_action_); + action_reg->insert(detector_action_); } else { @@ -29,6 +41,16 @@ ScoringParams::ScoringParams(inp::OpticalScoring input) } } +//---------------------------------------------------------------------------// +/*! + */ +void ScoringParams::process_hits(Span const& hits) const +{ + CELER_EXPECT(detector_callback_); + + (*detector_callback_)(hits); +} + //---------------------------------------------------------------------------// } // namespace optical } // namespace celeritas diff --git a/src/celeritas/optical/detector/ScoringParams.hh b/src/celeritas/optical/detector/ScoringParams.hh index 6dc3f6abc1..9d896d5e1e 100644 --- a/src/celeritas/optical/detector/ScoringParams.hh +++ b/src/celeritas/optical/detector/ScoringParams.hh @@ -6,22 +6,21 @@ //---------------------------------------------------------------------------// #pragma once +#include + #include "celeritas/inp/Scoring.hh" namespace celeritas { +class ActionRegistry; + namespace optical { +class DetectorAction; //---------------------------------------------------------------------------// /*! - * Brief class description. - * - * Optional detailed class description, and possibly example usage: - * \code - ScoringParams ...; - \endcode */ -class ScoringParams +class ScoringParams final { public: //!@{ @@ -30,10 +29,13 @@ class ScoringParams //!@} public: - ScoringParams(inp::OpticalScoring); + ScoringParams(ActionRegistry*, inp::OpticalScoring); + + void process_hits(Span const&) const; private: std::optional detector_callback_; + std::shared_ptr detector_action_; }; //---------------------------------------------------------------------------// diff --git a/src/celeritas/optical/detector/ScoringTrackView.hh b/src/celeritas/optical/detector/ScoringTrackView.hh new file mode 100644 index 0000000000..bcd9476355 --- /dev/null +++ b/src/celeritas/optical/detector/ScoringTrackView.hh @@ -0,0 +1,92 @@ +//------------------------------- -*- C++ -*- -------------------------------// +// Copyright Celeritas contributors: see top-level COPYRIGHT file for details +// SPDX-License-Identifier: (Apache-2.0 OR MIT) +//---------------------------------------------------------------------------// +//! \file celeritas/optical/detector/ScoringTrackView.hh +//---------------------------------------------------------------------------// +#pragma once + +#include "celeritas/Types.hh" + +#include "DetectorData.hh" + +namespace celeritas +{ +namespace optical +{ +//---------------------------------------------------------------------------// +/*! + * Brief class description. + * + * Optional detailed class description, and possibly example usage: + * \code + ScoringTrackView ...; + \endcode + */ +class ScoringTrackView +{ + public: + //!@{ + //! \name Type aliases + using StateRef = NativeRef; + //!@} + + public: + // Construct from local data + inline CELER_FUNCTION ScoringTrackView(StateRef const&, TrackSlotId); + + // Clear hit data for this track + inline CELER_FUNCTION void clear_hit(); + + // Score hit for this track + inline CELER_FUNCTION void score_hit(DetectorHit); + + // Get hit data associated with this track + inline CELER_FUNCTION DetectorHit& hit(); + + private: + StateRef const& state_; + TrackSlotId track_; +}; + +//---------------------------------------------------------------------------// +// INLINE DEFINITIONS +//---------------------------------------------------------------------------// +/*! + */ +CELER_FUNCTION +ScoringTrackView::ScoringTrackView(StateRef const& state, TrackSlotId tid) + : state_(state), track_(tid) +{ + CELER_EXPECT(tid); +} + +//---------------------------------------------------------------------------// +/*! + */ +CELER_FUNCTION void ScoringTrackView::clear_hit() +{ + this->hit().detector = {}; +} + +//---------------------------------------------------------------------------// +/*! + */ +CELER_FUNCTION void ScoringTrackView::score_hit(DetectorHit hit) +{ + CELER_EXPECT(hit.detector); + this->hit() = std::move(hit); +} + +//---------------------------------------------------------------------------// +/*! + */ +CELER_FUNCTION DetectorHit& ScoringTrackView::hit() +{ + CELER_EXPECT(track_ < state_.size()); + return state_.all_track_hits[track_]; +} + +//---------------------------------------------------------------------------// +} // namespace optical +} // namespace celeritas diff --git a/src/celeritas/setup/Problem.cc b/src/celeritas/setup/Problem.cc index 16a0c3b135..c0b4681dad 100644 --- a/src/celeritas/setup/Problem.cc +++ b/src/celeritas/setup/Problem.cc @@ -390,7 +390,8 @@ auto build_optical_params(inp::OpticalProblem const& p, pi.surface_physics = std::make_shared( pi.action_reg.get(), p.physics.surfaces); pi.detectors = std::move(loaded_model.detector); - pi.scoring = std::make_shared(p.scoring); + pi.scoring = std::make_shared(pi.action_reg.get(), + p.scoring); // Streams and capacities pi.max_streams = p.num_streams; diff --git a/test/celeritas/optical/Detector.test.cc b/test/celeritas/optical/Detector.test.cc index e976ee8cad..4bdf70aa74 100644 --- a/test/celeritas/optical/Detector.test.cc +++ b/test/celeritas/optical/Detector.test.cc @@ -42,32 +42,6 @@ class DetectorTest : public Test osi_.geant_setup = GeantOpticalPhysicsOptions::deactivated(); osi_.geant_setup.absorption = true; - osi_.problem.physics.surfaces = [] { - inp::SurfacePhysics input; - - // Center-top surface is only absorption - - PhysSurfaceId phys_surface{0}; - input.materials.push_back({}); - input.roughness.polished.emplace(phys_surface, inp::NoRoughness{}); - input.reflectivity.fresnel.emplace(phys_surface, - inp::FresnelReflection{}); - input.interaction.trivial.emplace( - phys_surface, optical::TrivialInteractionMode::absorb); - - // Default surface is transmission - - phys_surface++; - input.materials.push_back({}); - input.roughness.polished.emplace(phys_surface, inp::NoRoughness{}); - input.reflectivity.fresnel.emplace(phys_surface, - inp::FresnelReflection{}); - input.interaction.trivial.emplace( - phys_surface, optical::TrivialInteractionMode::transmit); - - return input; - }(); - osi_.problem.model.detectors.detectors = { {"y-detectors", {VolumeId{2}}}, {"x-detectors", {VolumeId{3}, VolumeId{4}}}, @@ -84,7 +58,7 @@ class DetectorTest : public Test * - Write a test to check bulk photon hits on device */ -struct SimpleScorer +struct SimpleScores { std::vector detector_ids; std::vector energies; @@ -93,26 +67,32 @@ struct SimpleScorer std::vector y_positions; std::vector z_positions; std::vector volume_instance_ids; +}; + +struct SimpleScorer +{ + SimpleScores& scores; void operator()(Span const& new_hits) { for (auto const& hit : new_hits) { - detector_ids.push_back(hit.detector.get()); - energies.push_back(value_as(hit.energy)); - times.push_back(hit.time); - x_positions.push_back(hit.position[0]); - y_positions.push_back(hit.position[1]); - z_positions.push_back(hit.position[2]); - volume_instance_ids.push_back(hit.volume_instance.get()); + scores.detector_ids.push_back(hit.detector.unchecked_get()); + scores.energies.push_back(value_as(hit.energy)); + scores.times.push_back(hit.time); + scores.x_positions.push_back(hit.position[0]); + scores.y_positions.push_back(hit.position[1]); + scores.z_positions.push_back(hit.position[2]); + scores.volume_instance_ids.push_back( + hit.volume_instance.unchecked_get()); } } }; TEST_F(DetectorTest, simple) { - SimpleScorer scores; - osi_.problem.scoring.detector_callback = scores; + SimpleScores scores; + osi_.problem.scoring.detector_callback = SimpleScorer{scores}; using E = units::MevEnergy; using TI = optical::TrackInitializer; @@ -153,13 +133,6 @@ TEST_F(DetectorTest, simple) 13, // time {}, ImplVolumeId{0}}, - TI{E{2e-6}, - Real3{0, 0, 0}, // pos - Real3{0, 1, 0}, // dir - Real3{1, 0, 0}, // pol - 2, // time - {}, - ImplVolumeId{0}}, TI{E{6e-6}, Real3{0, 0, 0}, // pos Real3{0, -1, 0}, // dir @@ -171,19 +144,28 @@ TEST_F(DetectorTest, simple) auto result = optical::Runner(std::move(osi_))(make_span(inits)); - EXPECT_EQ(0, result.counters.steps); - EXPECT_EQ(0, result.counters.step_iters); + EXPECT_EQ(6, result.counters.steps); + EXPECT_EQ(1, result.counters.step_iters); EXPECT_EQ(1, result.counters.flushes); ASSERT_EQ(1, result.counters.generators.size()); + real_type const flight_time = 1.66782047599076e-09; + static size_type const expected_detector_ids[] = {1, 1, 2, 2, 1, 0}; static real_type const expected_energies[] = {1e-6, 2e-6, 3e-6, 4e-6, 5e-6, 6e-6}; - static real_type const expected_x_positions[] = {0}; - static real_type const expected_y_positions[] = {0}; - static real_type const expected_z_positions[] = {0}; - static real_type const expected_times[] = {0}; - static size_type const expected_volume_instance_ids[] = {3, 4, 5, 6, 3, 2}; + static real_type const expected_x_positions[] = {50, -50, 0, 0, 50, 0}; + static real_type const expected_y_positions[] = {0, 0, 0, 0, 0, -50}; + static real_type const expected_z_positions[] = {0, 0, 50, -50, 0, 0}; + static real_type const expected_times[] = { + 0 + flight_time, + 10 + flight_time, + 1 + flight_time, + 20 + flight_time, + 13 + flight_time, + 7 + flight_time, + }; + static size_type const expected_volume_instance_ids[] = {5, 4, 6, 7, 5, 3}; EXPECT_VEC_EQ(expected_detector_ids, scores.detector_ids); EXPECT_VEC_SOFT_EQ(expected_energies, scores.energies); From 2572c71603ed9a24674e432c099ef31d0785c630 Mon Sep 17 00:00:00 2001 From: Hayden Hollenbeck Date: Wed, 28 Jan 2026 10:17:36 -0600 Subject: [PATCH 04/11] Stress test failing with OSI, switching to explicit setup --- test/celeritas/CMakeLists.txt | 1 + test/celeritas/optical/Detector.test.cc | 94 ++++++++++++++----- .../SurfacePhysicsIntegrationTestBase.cc | 33 +++---- test/geocel/data/optical-box.gdml | 7 -- 4 files changed, 87 insertions(+), 48 deletions(-) diff --git a/test/celeritas/CMakeLists.txt b/test/celeritas/CMakeLists.txt index 70a363d4ab..bb6295abf5 100644 --- a/test/celeritas/CMakeLists.txt +++ b/test/celeritas/CMakeLists.txt @@ -443,6 +443,7 @@ celeritas_add_standalone_tests(optical/Generator LArSphereGeneratorTest primary) celeritas_add_standalone_tests(optical/Generator LArSphereGeneratorTest direct) celeritas_add_standalone_tests(optical/Generator LArSphereGeneratorTest offload) celeritas_add_standalone_tests(optical/Detector DetectorTest simple) +celeritas_add_standalone_tests(optical/Detector DetectorTest stress) celeritas_add_test(optical/Absorption.test.cc) celeritas_add_test(optical/Cherenkov.test.cc) diff --git a/test/celeritas/optical/Detector.test.cc b/test/celeritas/optical/Detector.test.cc index 4bdf70aa74..39532de639 100644 --- a/test/celeritas/optical/Detector.test.cc +++ b/test/celeritas/optical/Detector.test.cc @@ -28,10 +28,9 @@ class DetectorTest : public Test osi_.problem.model.geometry = Test::test_data_path("geocel", "optical-box.gdml"); - osi_.problem.generator = inp::OpticalDirectGenerator{}; osi_.problem.capacity = [] { inp::OpticalStateCapacity cap; - cap.tracks = 32; + cap.tracks = 8; cap.primaries = 8 * cap.tracks; cap.generators = 2 * cap.tracks; return cap; @@ -43,21 +42,28 @@ class DetectorTest : public Test osi_.geant_setup.absorption = true; osi_.problem.model.detectors.detectors = { - {"y-detectors", {VolumeId{2}}}, + {"y-detectors", {VolumeId{1}, VolumeId{2}}}, {"x-detectors", {VolumeId{3}, VolumeId{4}}}, {"z-detectors", {VolumeId{5}, VolumeId{6}}}, }; + + osi_.problem.physics.surfaces = [] { + inp::SurfacePhysics input; + input.materials.push_back({}); + input.roughness.polished.emplace(PhysSurfaceId{0}, + inp::NoRoughness{}); + input.reflectivity.fresnel.emplace(PhysSurfaceId{0}, + inp::FresnelReflection{}); + input.interaction.trivial.emplace( + PhysSurfaceId{0}, optical::TrivialInteractionMode::transmit); + return input; + }(); } protected: inp::OpticalStandaloneInput osi_; }; -/* - * - Write a test to check bulk photon hits - * - Write a test to check bulk photon hits on device - */ - struct SimpleScores { std::vector detector_ids; @@ -141,6 +147,7 @@ TEST_F(DetectorTest, simple) {}, ImplVolumeId{0}}, }; + osi_.problem.generator = inp::OpticalDirectGenerator{}; auto result = optical::Runner(std::move(osi_))(make_span(inits)); @@ -176,31 +183,68 @@ TEST_F(DetectorTest, simple) EXPECT_VEC_EQ(expected_volume_instance_ids, scores.volume_instance_ids); } -TEST_F(DetectorTest, stress) +struct StressScorer { - size_type num_tracks = osi_.problem.capacity.tracks * 4; - - std::vector const inits( - num_tracks, - optical::TrackInitializer{units::MevEnergy{3e-6}, - from_cm(Real3{0, 49, 0}), - Real3{0, -1, 0}, // direction - Real3{0, 0, 1}, // polarization - 0, - {}, // primary - ImplVolumeId{0}}); + size_type& x_hits; + size_type& y_hits; + size_type& z_hits; + size_type& errored; - auto result = optical::Runner(std::move(osi_))(make_span(inits)); + void operator()(Span const& hits) + { + std::cout << "Num incoming hits: " << hits.size() << "\n"; + for (auto const& hit : hits) + { + if (hit.detector == DetectorId{0}) + { + y_hits++; + } + else if (hit.detector == DetectorId{1}) + { + x_hits++; + } + else if (hit.detector == DetectorId{2}) + { + z_hits++; + } + else + { + errored++; + } + } + std::cout << "Done processing hits!\n"; + } +}; + +TEST_F(DetectorTest, stress) +{ + size_type x_hits = 0; + size_type y_hits = 0; + size_type z_hits = 0; + size_type errored = 0; + osi_.problem.scoring.detector_callback + = StressScorer{x_hits, y_hits, z_hits, errored}; + + osi_.problem.generator = [&] { + inp::OpticalPrimaryGenerator gen; + gen.primaries = 9; + gen.energy = inp::MonoenergeticDistribution{1e-5}; + gen.angle = inp::IsotropicDistribution{}; + gen.shape = inp::PointDistribution{{0, 0, 0}}; + return gen; + }(); + + auto result = optical::Runner(std::move(osi_))(); EXPECT_EQ(0, result.counters.steps); EXPECT_EQ(0, result.counters.step_iters); EXPECT_EQ(1, result.counters.flushes); ASSERT_EQ(1, result.counters.generators.size()); - auto const& gen = result.counters.generators.front(); - EXPECT_EQ(num_tracks, gen.buffer_size); - EXPECT_EQ(0, gen.num_pending); - EXPECT_EQ(num_tracks, gen.num_generated); + EXPECT_EQ(x_hits, 0); + EXPECT_EQ(y_hits, 0); + EXPECT_EQ(z_hits, 0); + EXPECT_EQ(errored, 0); } //---------------------------------------------------------------------------// diff --git a/test/celeritas/optical/SurfacePhysicsIntegrationTestBase.cc b/test/celeritas/optical/SurfacePhysicsIntegrationTestBase.cc index 58fe75a61b..5c07826106 100644 --- a/test/celeritas/optical/SurfacePhysicsIntegrationTestBase.cc +++ b/test/celeritas/optical/SurfacePhysicsIntegrationTestBase.cc @@ -51,22 +51,23 @@ auto SurfacePhysicsIntegrationTestBase::build_optical_surface_physics() this->setup_surface_models(input); - // Default surface - - PhysSurfaceId phys_surface = [&] { - size_type num_surfaces = 0; - for (auto const& mats : input.materials) - { - num_surfaces += mats.size() + 1; - } - return PhysSurfaceId(num_surfaces); - }(); - - input.materials.push_back({}); - input.roughness.polished.emplace(phys_surface, inp::NoRoughness{}); - input.reflectivity.fresnel.emplace(phys_surface, inp::FresnelReflection{}); - input.interaction.trivial.emplace(phys_surface, - TrivialInteractionMode::absorb); + // // Default surface + + // PhysSurfaceId phys_surface = [&] { + // size_type num_surfaces = 0; + // for (auto const& mats : input.materials) + // { + // num_surfaces += mats.size() + 1; + // } + // return PhysSurfaceId(num_surfaces); + // }(); + + // input.materials.push_back({}); + // input.roughness.polished.emplace(phys_surface, inp::NoRoughness{}); + // input.reflectivity.fresnel.emplace(phys_surface, + // inp::FresnelReflection{}); + // input.interaction.trivial.emplace(phys_surface, + // TrivialInteractionMode::absorb); return std::make_shared( this->optical_action_reg().get(), input); diff --git a/test/geocel/data/optical-box.gdml b/test/geocel/data/optical-box.gdml index 94e7085c25..d486c4a3fd 100644 --- a/test/geocel/data/optical-box.gdml +++ b/test/geocel/data/optical-box.gdml @@ -35,9 +35,6 @@ - - - @@ -104,10 +101,6 @@ - - - - From cdfa83372589890bf8164e9f9b3362e67b506d86 Mon Sep 17 00:00:00 2001 From: Hayden Hollenbeck Date: Wed, 28 Jan 2026 15:55:12 -0600 Subject: [PATCH 05/11] Got both tests passing with import data method --- test/celeritas/GlobalTestBase.cc | 7 +- test/celeritas/GlobalTestBase.hh | 8 + test/celeritas/ImportedDataTestBase.cc | 8 + test/celeritas/ImportedDataTestBase.hh | 1 + test/celeritas/OnlyCoreTestBase.hh | 4 + test/celeritas/optical/Detector.test.cc | 211 ++++++++++-------- test/celeritas/optical/OpticalMockTestBase.hh | 4 + 7 files changed, 152 insertions(+), 91 deletions(-) diff --git a/test/celeritas/GlobalTestBase.cc b/test/celeritas/GlobalTestBase.cc index 58f9ae37d9..0ef35c7416 100644 --- a/test/celeritas/GlobalTestBase.cc +++ b/test/celeritas/GlobalTestBase.cc @@ -195,6 +195,7 @@ optical::CoreParams::Input GlobalTestBase::optical_params_input() inp.sim = this->optical_sim(); inp.surface_physics = this->optical_surface_physics(); inp.detectors = this->detector(); + inp.scoring = this->optical_scoring(); inp.cherenkov = this->cherenkov(); inp.scintillation = this->scintillation(); inp.capacity = inp::OpticalStateCapacity::from_default( @@ -235,9 +236,9 @@ auto GlobalTestBase::build_core() -> SPConstCore inp.physics = this->physics(); inp.rng = this->rng(); inp.sim = this->sim(); - inp.surface = surface_; - inp.volume = volume_; - inp.detectors = detector_; + inp.surface = this->surface(); + inp.volume = this->volume(); + inp.detectors = this->detector(); inp.wentzel = this->wentzel(); inp.action_reg = this->action_reg(); diff --git a/test/celeritas/GlobalTestBase.hh b/test/celeritas/GlobalTestBase.hh index 99d238c6bf..79111f1a60 100644 --- a/test/celeritas/GlobalTestBase.hh +++ b/test/celeritas/GlobalTestBase.hh @@ -57,6 +57,7 @@ namespace optical class MaterialParams; class PhysicsParams; class SurfacePhysicsParams; +class ScoringParams; } // namespace optical namespace test @@ -104,6 +105,7 @@ class GlobalTestBase : public Test, public LazyGeantGeoManager using SPConstOpticalMaterial = SP; using SPOpticalParams = SP; using SPConstOpticalPhysics = SP; + using SPConstOpticalScoring = SP; using SPConstOpticalSim = SP; using SPConstOpticalSurfacePhysics = SP; @@ -142,6 +144,7 @@ class GlobalTestBase : public Test, public LazyGeantGeoManager inline SPConstOpticalMaterial const& optical_material(); inline SPOpticalParams const& optical_params(); inline SPConstOpticalPhysics const& optical_physics(); + inline SPConstOpticalScoring const& optical_scoring(); inline SPConstOpticalSim const& optical_sim(); inline SPConstOpticalSurfacePhysics const& optical_surface_physics(); inline SPConstScintillation const& scintillation(); @@ -165,6 +168,7 @@ class GlobalTestBase : public Test, public LazyGeantGeoManager inline SPConstOpticalMaterial const& optical_material() const; inline SPOpticalParams const& optical_params() const; inline SPConstOpticalPhysics const& optical_physics() const; + inline SPConstOpticalScoring const& optical_scoring() const; inline SPConstOpticalSim const& optical_sim() const; inline SPConstOpticalSurfacePhysics const& optical_surface_physics() const; inline SPConstScintillation const& scintillation() const; @@ -200,6 +204,7 @@ class GlobalTestBase : public Test, public LazyGeantGeoManager [[nodiscard]] virtual SPConstCherenkov build_cherenkov() = 0; [[nodiscard]] virtual SPConstOpticalMaterial build_optical_material() = 0; [[nodiscard]] virtual SPConstOpticalPhysics build_optical_physics() = 0; + [[nodiscard]] virtual SPConstOpticalScoring build_optical_scoring() = 0; [[nodiscard]] virtual SPConstOpticalSim build_optical_sim() = 0; [[nodiscard]] virtual SPConstOpticalSurfacePhysics build_optical_surface_physics() @@ -213,6 +218,7 @@ class GlobalTestBase : public Test, public LazyGeantGeoManager SPConstSurface const& surface() const { return surface_; } SPConstVolume const& volume() const { return volume_; } SPConstDetectors const& detector() const { return detector_; } + virtual SPConstDetectors detector() { return detector_; } // Implement LazyGeantGeoManager SPConstGeoI build_geo_from_geant(SPConstGeantGeo const&) const final; @@ -255,6 +261,7 @@ class GlobalTestBase : public Test, public LazyGeantGeoManager SPConstOpticalMaterial optical_material_; SPOpticalParams optical_params_; SPConstOpticalPhysics optical_physics_; + SPConstOpticalScoring optical_scoring_; SPConstOpticalSim optical_sim_; SPConstOpticalSurfacePhysics optical_surface_physics_; SPConstScintillation scintillation_; @@ -317,6 +324,7 @@ DEF_GTB_ACCESSORS(SPOpticalParams, optical_params) DEF_GTB_ACCESSORS(SPConstOpticalPhysics, optical_physics) DEF_GTB_ACCESSORS(SPConstOpticalSim, optical_sim) DEF_GTB_ACCESSORS(SPConstOpticalSurfacePhysics, optical_surface_physics) +DEF_OPTIONAL_GTB_ACCESSORS(SPConstOpticalScoring, optical_scoring) DEF_OPTIONAL_GTB_ACCESSORS(SPConstScintillation, scintillation) DEF_OPTIONAL_GTB_ACCESSORS(SPConstWentzelOKVI, wentzel) diff --git a/test/celeritas/ImportedDataTestBase.cc b/test/celeritas/ImportedDataTestBase.cc index c885c1a621..09f3ed9c84 100644 --- a/test/celeritas/ImportedDataTestBase.cc +++ b/test/celeritas/ImportedDataTestBase.cc @@ -179,6 +179,14 @@ auto ImportedDataTestBase::build_optical_physics() -> SPConstOpticalPhysics return std::make_shared(std::move(input)); } +//---------------------------------------------------------------------------// +auto ImportedDataTestBase::build_optical_scoring() -> SPConstOpticalScoring +{ + // Optical scoring is just a user callback for optical hits, which isn't + // directly supported from import data. + return nullptr; +} + //---------------------------------------------------------------------------// auto ImportedDataTestBase::build_optical_sim() -> SPConstOpticalSim { diff --git a/test/celeritas/ImportedDataTestBase.hh b/test/celeritas/ImportedDataTestBase.hh index 7807723425..a48d3dacfa 100644 --- a/test/celeritas/ImportedDataTestBase.hh +++ b/test/celeritas/ImportedDataTestBase.hh @@ -51,6 +51,7 @@ class ImportedDataTestBase : virtual public GlobalTestBase SPConstCherenkov build_cherenkov() override; SPConstOpticalMaterial build_optical_material() override; SPConstOpticalPhysics build_optical_physics() override; + SPConstOpticalScoring build_optical_scoring() override; SPConstOpticalSim build_optical_sim() override; SPConstOpticalSurfacePhysics build_optical_surface_physics() override; SPConstScintillation build_scintillation() override; diff --git a/test/celeritas/OnlyCoreTestBase.hh b/test/celeritas/OnlyCoreTestBase.hh index 76913d8250..c0ce2f3384 100644 --- a/test/celeritas/OnlyCoreTestBase.hh +++ b/test/celeritas/OnlyCoreTestBase.hh @@ -31,6 +31,10 @@ class OnlyCoreTestBase : virtual public GlobalTestBase { CELER_ASSERT_UNREACHABLE(); } + SPConstOpticalScoring build_optical_scoring() override + { + CELER_ASSERT_UNREACHABLE(); + } SPConstOpticalSim build_optical_sim() override { CELER_ASSERT_UNREACHABLE(); diff --git a/test/celeritas/optical/Detector.test.cc b/test/celeritas/optical/Detector.test.cc index 39532de639..4a420a714d 100644 --- a/test/celeritas/optical/Detector.test.cc +++ b/test/celeritas/optical/Detector.test.cc @@ -8,60 +8,115 @@ #include #include "geocel/UnitUtils.hh" +#include "geocel/VolumeParams.hh" +#include "geocel/inp/Model.hh" +#include "celeritas/GeantTestBase.hh" +#include "celeritas/global/CoreParams.hh" #include "celeritas/inp/StandaloneInput.hh" -#include "celeritas/optical/Runner.hh" +#include "celeritas/optical/CoreParams.hh" +#include "celeritas/optical/CoreState.hh" +#include "celeritas/optical/Transporter.hh" #include "celeritas/optical/Types.hh" #include "celeritas/optical/detector/DetectorData.hh" +#include "celeritas/optical/detector/ScoringParams.hh" +#include "celeritas/optical/gen/DirectGeneratorAction.hh" +#include "celeritas/optical/gen/PrimaryGeneratorAction.hh" +#include "celeritas/optical/surface/SurfacePhysicsParams.hh" #include "celeritas_test.hh" namespace celeritas { +namespace optical +{ namespace test { +using namespace ::celeritas::test; //---------------------------------------------------------------------------// -class DetectorTest : public Test +class DetectorTest : public ::celeritas::test::GeantTestBase { public: - void SetUp() override + std::string_view gdml_basename() const override { return "optical-box"; } + + GeantPhysicsOptions build_geant_options() const override + { + auto result = GeantTestBase::build_geant_options(); + result.optical = {}; + CELER_ENSURE(result.optical); + return result; + } + + GeantImportDataSelection build_import_data_selection() const override { - osi_.problem.model.geometry - = Test::test_data_path("geocel", "optical-box.gdml"); - - osi_.problem.capacity = [] { - inp::OpticalStateCapacity cap; - cap.tracks = 8; - cap.primaries = 8 * cap.tracks; - cap.generators = 2 * cap.tracks; - return cap; - }(); - - osi_.problem.num_streams = 1; - - osi_.geant_setup = GeantOpticalPhysicsOptions::deactivated(); - osi_.geant_setup.absorption = true; - - osi_.problem.model.detectors.detectors = { - {"y-detectors", {VolumeId{1}, VolumeId{2}}}, - {"x-detectors", {VolumeId{3}, VolumeId{4}}}, - {"z-detectors", {VolumeId{5}, VolumeId{6}}}, - }; - - osi_.problem.physics.surfaces = [] { - inp::SurfacePhysics input; - input.materials.push_back({}); - input.roughness.polished.emplace(PhysSurfaceId{0}, - inp::NoRoughness{}); - input.reflectivity.fresnel.emplace(PhysSurfaceId{0}, - inp::FresnelReflection{}); - input.interaction.trivial.emplace( - PhysSurfaceId{0}, optical::TrivialInteractionMode::transmit); - return input; - }(); + auto result = GeantTestBase::build_import_data_selection(); + result.processes |= GeantImportDataSelection::optical; + return result; + } + + std::vector select_optical_models() const override + { + return {IMC::absorption}; + } + + SPConstOpticalSurfacePhysics build_optical_surface_physics() override + { + PhysSurfaceId phys_surface{0}; + + inp::SurfacePhysics input; + input.materials.push_back({}); + input.roughness.polished.emplace(phys_surface, inp::NoRoughness{}); + input.reflectivity.fresnel.emplace(phys_surface, + inp::FresnelReflection{}); + input.interaction.trivial.emplace(phys_surface, + TrivialInteractionMode::transmit); + + return std::make_shared( + this->optical_action_reg().get(), input); + } + + SPConstDetectors detector() override + { + if (!detector_) + { + inp::Detectors input{{ + {"y-detectors", {VolumeId{1}, VolumeId{2}}}, + {"x-detectors", {VolumeId{3}, VolumeId{4}}}, + {"z-detectors", {VolumeId{5}, VolumeId{6}}}, + }}; + + detector_ = std::make_shared(std::move(input), + *this->volume()); + } + + return detector_; + } + + SPConstOpticalScoring build_optical_scoring() override + { + return std::make_shared( + this->optical_action_reg().get(), scoring_input_); + } + + void initialize_run() + { + Transporter::Input inp; + inp.params = this->optical_params(); + transport_ = std::make_shared(std::move(inp)); + + size_type num_tracks = 128; + state_ = std::make_shared>( + *this->optical_params(), StreamId{0}, num_tracks); + state_->aux() = std::make_shared( + *this->core()->aux_reg(), MemSpace::host, StreamId{0}, num_tracks); } protected: - inp::OpticalStandaloneInput osi_; + std::shared_ptr> state_; + std::shared_ptr aux_; + std::shared_ptr transport_; + std::shared_ptr detector_; + + inp::OpticalScoring scoring_input_; }; struct SimpleScores @@ -79,7 +134,7 @@ struct SimpleScorer { SimpleScores& scores; - void operator()(Span const& new_hits) + void operator()(Span const& new_hits) { for (auto const& hit : new_hits) { @@ -98,10 +153,10 @@ struct SimpleScorer TEST_F(DetectorTest, simple) { SimpleScores scores; - osi_.problem.scoring.detector_callback = SimpleScorer{scores}; + scoring_input_.detector_callback = SimpleScorer{scores}; using E = units::MevEnergy; - using TI = optical::TrackInitializer; + using TI = TrackInitializer; std::vector const inits{ TI{E{1e-6}, @@ -147,14 +202,12 @@ TEST_F(DetectorTest, simple) {}, ImplVolumeId{0}}, }; - osi_.problem.generator = inp::OpticalDirectGenerator{}; - auto result = optical::Runner(std::move(osi_))(make_span(inits)); - - EXPECT_EQ(6, result.counters.steps); - EXPECT_EQ(1, result.counters.step_iters); - EXPECT_EQ(1, result.counters.flushes); - ASSERT_EQ(1, result.counters.generators.size()); + auto generate + = DirectGeneratorAction::make_and_insert(*this->optical_params()); + this->initialize_run(); + generate->insert(*state_, make_span(inits)); + (*transport_)(*state_); real_type const flight_time = 1.66782047599076e-09; @@ -185,68 +238,50 @@ TEST_F(DetectorTest, simple) struct StressScorer { - size_type& x_hits; - size_type& y_hits; - size_type& z_hits; + std::vector& scores; size_type& errored; - void operator()(Span const& hits) + void operator()(Span const& hits) { - std::cout << "Num incoming hits: " << hits.size() << "\n"; for (auto const& hit : hits) { - if (hit.detector == DetectorId{0}) - { - y_hits++; - } - else if (hit.detector == DetectorId{1}) - { - x_hits++; - } - else if (hit.detector == DetectorId{2}) + if (hit.detector < scores.size()) { - z_hits++; + scores[hit.detector.get()]++; } else { errored++; } } - std::cout << "Done processing hits!\n"; } }; TEST_F(DetectorTest, stress) { - size_type x_hits = 0; - size_type y_hits = 0; - size_type z_hits = 0; + std::vector hits(3, 0); size_type errored = 0; - osi_.problem.scoring.detector_callback - = StressScorer{x_hits, y_hits, z_hits, errored}; - - osi_.problem.generator = [&] { - inp::OpticalPrimaryGenerator gen; - gen.primaries = 9; - gen.energy = inp::MonoenergeticDistribution{1e-5}; - gen.angle = inp::IsotropicDistribution{}; - gen.shape = inp::PointDistribution{{0, 0, 0}}; - return gen; - }(); - - auto result = optical::Runner(std::move(osi_))(); - - EXPECT_EQ(0, result.counters.steps); - EXPECT_EQ(0, result.counters.step_iters); - EXPECT_EQ(1, result.counters.flushes); - ASSERT_EQ(1, result.counters.generators.size()); - - EXPECT_EQ(x_hits, 0); - EXPECT_EQ(y_hits, 0); - EXPECT_EQ(z_hits, 0); + scoring_input_.detector_callback = StressScorer{hits, errored}; + + inp::OpticalPrimaryGenerator gen; + gen.primaries = 8192; + gen.energy = inp::MonoenergeticDistribution{1e-5}; + gen.angle = inp::IsotropicDistribution{}; + gen.shape = inp::PointDistribution{{0, 0, 0}}; + + auto generate = PrimaryGeneratorAction::make_and_insert( + *this->optical_params(), std::move(gen)); + this->initialize_run(); + generate->insert(*state_); + (*transport_)(*state_); + + static size_type const expected_hits[] = {2673, 2816, 2703}; + + EXPECT_VEC_EQ(expected_hits, hits); EXPECT_EQ(errored, 0); } //---------------------------------------------------------------------------// } // namespace test +} // namespace optical } // namespace celeritas diff --git a/test/celeritas/optical/OpticalMockTestBase.hh b/test/celeritas/optical/OpticalMockTestBase.hh index 2533cc3b8c..da9e3b6c6f 100644 --- a/test/celeritas/optical/OpticalMockTestBase.hh +++ b/test/celeritas/optical/OpticalMockTestBase.hh @@ -67,6 +67,10 @@ class OpticalMockTestBase : public GlobalTestBase { CELER_ASSERT_UNREACHABLE(); } + SPConstOpticalScoring build_optical_scoring() override + { + CELER_ASSERT_UNREACHABLE(); + } SPConstOpticalSim build_optical_sim() override { CELER_ASSERT_UNREACHABLE(); From 9a3c697cb424e14ccca9ddf7f680142be571ef1a Mon Sep 17 00:00:00 2001 From: Hayden Hollenbeck Date: Wed, 28 Jan 2026 16:51:33 -0600 Subject: [PATCH 06/11] Add documentation and cleanup --- .../optical/detector/DetectorAction.hh | 26 +++++---- .../optical/detector/DetectorData.cc | 10 +++- .../optical/detector/DetectorData.cu | 20 +++++-- .../optical/detector/DetectorData.hh | 58 ++++++++++++------- .../optical/detector/DetectorExecutor.hh | 11 ++++ .../optical/detector/ScoringParams.cc | 16 +++-- .../optical/detector/ScoringParams.hh | 7 +++ .../optical/detector/ScoringTrackView.hh | 18 ++++-- src/celeritas/setup/StandaloneInput.cc | 10 +--- test/celeritas/ImportedDataTestBase.cc | 7 ++- test/celeritas/optical/Detector.test.cc | 30 ++++++++++ .../SurfacePhysicsIntegrationTestBase.cc | 18 ------ 12 files changed, 150 insertions(+), 81 deletions(-) diff --git a/src/celeritas/optical/detector/DetectorAction.hh b/src/celeritas/optical/detector/DetectorAction.hh index 7dbfd03f2c..7a28b9a121 100644 --- a/src/celeritas/optical/detector/DetectorAction.hh +++ b/src/celeritas/optical/detector/DetectorAction.hh @@ -21,21 +21,18 @@ namespace optical { //---------------------------------------------------------------------------// /*! - * Brief class description. + * Record sensitive detector data for optical photons at the end of every step. * - * Optional detailed class description, and possibly example usage: - * \code - DetectorAction ...; - \endcode + * The \c DetectorExecutor is responsible for copying hit data for every photon + * into the state buffer at the end of every step on a kernel level. Even if a + * track was not in a detector, it is still copied into the state buffer with + * an invalid detector ID. All hits are copied into pinned memory on the host, + * where invalid hits are erased. A span of only valid hits is then passed into + * the user provided callback function. */ class DetectorAction final : public OpticalStepActionInterface, public StaticConcreteAction { - public: - //!@{ - //! \name Type aliases - //!@} - public: // Construct with ID explicit DetectorAction(ActionId); @@ -50,6 +47,7 @@ class DetectorAction final : public OpticalStepActionInterface, StepActionOrder order() const final { return StepActionOrder::post; } private: + // Process hits copied from the kernels and send them to the callback template void process_hits(CoreParams const&, CoreState&) const; }; @@ -58,6 +56,11 @@ class DetectorAction final : public OpticalStepActionInterface, // INLINE DEFINITIONS //---------------------------------------------------------------------------// /*! + * Process hits copied from the kernels and send them to the callback. + * + * Copied hits might be invalid, and are removed before sending into the + * callback function. The callback is only execute when a non-zero amount of + * valid hits occurs. */ template void DetectorAction::process_hits(CoreParams const& params, @@ -66,7 +69,7 @@ void DetectorAction::process_hits(CoreParams const& params, DetectorHitOutput hit_results; // Copy hits (possibly from device) into pinned vector - copy_hits(&hit_results, state.ref().scoring); + copy_hits(&hit_results, state.ref().scoring, state.stream_id()); // Erase all hits with invalid detector ID hit_results.hits.erase( @@ -77,6 +80,7 @@ void DetectorAction::process_hits(CoreParams const& params, }), hit_results.hits.end()); + // Send hits to the callback function, if there are any if (!hit_results.hits.empty()) { auto scoring = params.scoring(); diff --git a/src/celeritas/optical/detector/DetectorData.cc b/src/celeritas/optical/detector/DetectorData.cc index 0dccfcbde6..01272a94ce 100644 --- a/src/celeritas/optical/detector/DetectorData.cc +++ b/src/celeritas/optical/detector/DetectorData.cc @@ -13,11 +13,17 @@ namespace celeritas namespace optical { //---------------------------------------------------------------------------// - +/*! + * Copy hits from host state data to pinned memory. + * + * Because both buffer reside host-side, this is just a trivial copy between + * the buffers. + */ template<> void copy_hits( DetectorHitOutput* output, - DetectorStateData const& state) + DetectorStateData const& state, + StreamId /* unused */) { // Trivial copy to pinned memory output->hits.resize(state.all_track_hits.size()); diff --git a/src/celeritas/optical/detector/DetectorData.cu b/src/celeritas/optical/detector/DetectorData.cu index 7d56a40f36..1955262a4e 100644 --- a/src/celeritas/optical/detector/DetectorData.cu +++ b/src/celeritas/optical/detector/DetectorData.cu @@ -11,23 +11,33 @@ namespace celeritas namespace optical { //---------------------------------------------------------------------------// +/*! + * Copy hits from device to pinned memory. + * + * All hits from all tracks are copied. These may include invalid hits where a + * track is not in a detector, which is indicated by an invalid detector ID. + * The user of the output is therefore responsible for parsing the pinned + * memory for only valid hits. + */ template<> copy_hits(DetectorHitOutput* output, - DetectorStateData const& state) + DetectorStateData const& state, + StreamId stream_id) { CELER_EXPECT(output); - - // Trivially copy all track hits from device + CELER_EXPECT(stream_id); size_type num_tracks = state.all_track_hits.size(); + // Copy all track hits from device output->hits.resize(num_tracks); Copier copy{{output->hits.data(), num_tracks}, - state.stream_id}; + stream_id}; copy(MemSpace::device, {state.all_track_hits.data().get(), num_tracks}); + // Synchronize to ensure all data is transferred before continuing CELER_DEVICE_API_CALL( - StreamSynchronize(celeritas::device().stream(state.stream_id).get())); + StreamSynchronize(celeritas::device().stream(stream_id).get())); CELER_ENSURE(output->hits.size() == num_tracks); } diff --git a/src/celeritas/optical/detector/DetectorData.hh b/src/celeritas/optical/detector/DetectorData.hh index 53b7f9bfc8..4019c3bd8a 100644 --- a/src/celeritas/optical/detector/DetectorData.hh +++ b/src/celeritas/optical/detector/DetectorData.hh @@ -19,6 +19,7 @@ namespace optical { //---------------------------------------------------------------------------// /*! + * A single hit of a photon track on a sensitive detector. */ struct DetectorHit { @@ -33,17 +34,25 @@ struct DetectorHit //---------------------------------------------------------------------------// /*! + * Pinned memory buffer for transferring detector hits. */ struct DetectorHitOutput { + //!@{ + //! \name Type aliases template using PinnedVec = std::vector>; + //!@} PinnedVec hits; }; //---------------------------------------------------------------------------// /*! + * State buffer for storing detector hits. + * + * Detector hits is large enough to store a hit for every track at the end of a + * step. Stored hits may be invalid if the track is not in a detector region. */ template struct DetectorStateData @@ -54,7 +63,6 @@ struct DetectorStateData using StateItems = StateCollection; //!@} - StreamId stream_id{}; StateItems all_track_hits; //! Whether data is assigned and valid @@ -71,52 +79,58 @@ struct DetectorStateData DetectorStateData& operator=(DetectorStateData& other) { CELER_EXPECT(other); - stream_id = other.stream_id; all_track_hits = other.all_track_hits; return *this; } }; //---------------------------------------------------------------------------// -/*! - * Resize the state in host code. - */ -template -inline void -resize(DetectorStateData* state, size_type size) -{ - CELER_EXPECT(state); - CELER_EXPECT(size > 0); - - resize(&state->all_track_hits, size); - - CELER_ENSURE(*state); -} - -//---------------------------------------------------------------------------// +// Copy hits from a memory space to pinned memory template void copy_hits(DetectorHitOutput* output, - DetectorStateData const& state); + DetectorStateData const& state, + StreamId); template<> void copy_hits(DetectorHitOutput* output, - DetectorStateData const&); + DetectorStateData const&, + StreamId); template<> void copy_hits(DetectorHitOutput* output, - DetectorStateData const&); + DetectorStateData const&, + StreamId); #if !CELER_USE_DEVICE template<> inline void copy_hits(DetectorHitOutput* output, - DetectorStateData const&) + DetectorStateData const&, + StreamId) { CELER_NOT_CONFIGURED("CUDA OR HIP"); } #endif +//---------------------------------------------------------------------------// +// INLINE DEFINITIONS +//---------------------------------------------------------------------------// +/*! + * Resize the state in host code. + */ +template +inline void +resize(DetectorStateData* state, size_type size) +{ + CELER_EXPECT(state); + CELER_EXPECT(size > 0); + + resize(&state->all_track_hits, size); + + CELER_ENSURE(*state); +} + //---------------------------------------------------------------------------// } // namespace optical } // namespace celeritas diff --git a/src/celeritas/optical/detector/DetectorExecutor.hh b/src/celeritas/optical/detector/DetectorExecutor.hh index c1976466de..ced9542eff 100644 --- a/src/celeritas/optical/detector/DetectorExecutor.hh +++ b/src/celeritas/optical/detector/DetectorExecutor.hh @@ -12,9 +12,17 @@ namespace optical { //---------------------------------------------------------------------------// /*! + * Populate detector state buffer at the end of a step. + * + * All tracks have hits copied into the state buffer. If the track is not alive + * or is not in a detector region, an invalid hit is set in the corresponding + * buffer track slot. + * + * When a track generates a valid hit, it is killed (absorbed by the detector). */ struct DetectorExecutor { + // Copy track hit into the state buffer inline CELER_FUNCTION void operator()(CoreTrackView const&) const; }; @@ -22,6 +30,7 @@ struct DetectorExecutor // INLINE DEFINITIONS //---------------------------------------------------------------------------// /*! + * Copy track hit into the state buffer. */ CELER_FUNCTION void DetectorExecutor::operator()(CoreTrackView const& track) const @@ -40,6 +49,7 @@ DetectorExecutor::operator()(CoreTrackView const& track) const if (detector_id) { + // Score a valid hit score.score_hit(DetectorHit{detector_id, track.particle().energy(), sim.time(), @@ -51,6 +61,7 @@ DetectorExecutor::operator()(CoreTrackView const& track) const } else { + // Mark that the track is not in a detector score.clear_hit(); } } diff --git a/src/celeritas/optical/detector/ScoringParams.cc b/src/celeritas/optical/detector/ScoringParams.cc index 8f8cc841ec..373036378b 100644 --- a/src/celeritas/optical/detector/ScoringParams.cc +++ b/src/celeritas/optical/detector/ScoringParams.cc @@ -19,6 +19,13 @@ namespace optical { //---------------------------------------------------------------------------// /*! + * Construct scoring parameters from input. + * + * Registers the \c DetectorAction as a post-step action and stores the + * callback function for the \c DetectorAction to send hits to. + * + * If no callback function is provided, then \c DetectorAction is not + * registered. */ ScoringParams::ScoringParams(ActionRegistry* action_reg, inp::OpticalScoring input) @@ -28,21 +35,18 @@ ScoringParams::ScoringParams(ActionRegistry* action_reg, if (detector_callback_) { - CELER_LOG(info) << "optical scoring enabled."; - detector_action_ = std::make_shared(action_reg->next_id()); CELER_ASSERT(detector_action_); action_reg->insert(detector_action_); } - else - { - CELER_LOG(info) << "optical scoring disabled."; - } } //---------------------------------------------------------------------------// /*! + * Send hits to the user provided callback function. + * + * Must be built with a user callback function. */ void ScoringParams::process_hits(Span const& hits) const { diff --git a/src/celeritas/optical/detector/ScoringParams.hh b/src/celeritas/optical/detector/ScoringParams.hh index 9d896d5e1e..d758c461c8 100644 --- a/src/celeritas/optical/detector/ScoringParams.hh +++ b/src/celeritas/optical/detector/ScoringParams.hh @@ -19,6 +19,11 @@ namespace optical class DetectorAction; //---------------------------------------------------------------------------// /*! + * Manages user callback for optical detectors. + * + * Constructs the \c DetectorAction used by sensitive detectors to send hits + * back to the user provided callback function. If the callback function is not + * provided, then no \c DetectorAction is created. */ class ScoringParams final { @@ -29,8 +34,10 @@ class ScoringParams final //!@} public: + // Construct from optical scoring input ScoringParams(ActionRegistry*, inp::OpticalScoring); + // Send hits to user callback function void process_hits(Span const&) const; private: diff --git a/src/celeritas/optical/detector/ScoringTrackView.hh b/src/celeritas/optical/detector/ScoringTrackView.hh index bcd9476355..0abf059a42 100644 --- a/src/celeritas/optical/detector/ScoringTrackView.hh +++ b/src/celeritas/optical/detector/ScoringTrackView.hh @@ -16,12 +16,12 @@ namespace optical { //---------------------------------------------------------------------------// /*! - * Brief class description. + * Track view into the corresponding hit buffer. * - * Optional detailed class description, and possibly example usage: - * \code - ScoringTrackView ...; - \endcode + * For a given track ID, this view accesses the corresponding hit data in the + * detector state buffer. For tracks in detectors, the \c score_hit function + * may be used to populate the track's hit data. Otherwise, the track's hit + * data should be cleared at the end of every step with \c clear_hit. */ class ScoringTrackView { @@ -53,6 +53,7 @@ class ScoringTrackView // INLINE DEFINITIONS //---------------------------------------------------------------------------// /*! + * Construct from detector state buffer and track ID. */ CELER_FUNCTION ScoringTrackView::ScoringTrackView(StateRef const& state, TrackSlotId tid) @@ -63,6 +64,9 @@ ScoringTrackView::ScoringTrackView(StateRef const& state, TrackSlotId tid) //---------------------------------------------------------------------------// /*! + * Clear hit data for this track. + * + * Marks the hit with an invalid detector ID. */ CELER_FUNCTION void ScoringTrackView::clear_hit() { @@ -71,6 +75,9 @@ CELER_FUNCTION void ScoringTrackView::clear_hit() //---------------------------------------------------------------------------// /*! + * Set the hit data for this track. + * + * Should have a valid detector ID to indicate it is a valid hit. */ CELER_FUNCTION void ScoringTrackView::score_hit(DetectorHit hit) { @@ -80,6 +87,7 @@ CELER_FUNCTION void ScoringTrackView::score_hit(DetectorHit hit) //---------------------------------------------------------------------------// /*! + * Access the hit data associated with this track. */ CELER_FUNCTION DetectorHit& ScoringTrackView::hit() { diff --git a/src/celeritas/setup/StandaloneInput.cc b/src/celeritas/setup/StandaloneInput.cc index 5c6c91dfa6..fa7ebda0ff 100644 --- a/src/celeritas/setup/StandaloneInput.cc +++ b/src/celeritas/setup/StandaloneInput.cc @@ -138,15 +138,7 @@ OpticalStandaloneLoaded standalone_input(inp::OpticalStandaloneInput& si) // Load geometry, surfaces, regions from Geant4 world pointer CELER_ASSERT(geant_setup.geo_params()); - { - // TODO: have a better way to define detectors for standalone optical - // simulations. - CELER_LOG(warning) << "Sensitive detectors from GDML not currently " - "supported! Please be careful!"; - auto dets = si.problem.model.detectors; - si.problem.model = geant_setup.geo_params()->make_model_input(); - si.problem.model.detectors = std::move(dets); - } + si.problem.model = geant_setup.geo_params()->make_model_input(); // Import optical physics data from Geant4 ImportData imported; diff --git a/test/celeritas/ImportedDataTestBase.cc b/test/celeritas/ImportedDataTestBase.cc index 09f3ed9c84..bf513128e7 100644 --- a/test/celeritas/ImportedDataTestBase.cc +++ b/test/celeritas/ImportedDataTestBase.cc @@ -9,12 +9,14 @@ #include "geocel/SurfaceParams.hh" #include "celeritas/em/params/WentzelOKVIParams.hh" #include "celeritas/geo/GeoMaterialParams.hh" +#include "celeritas/inp/Scoring.hh" #include "celeritas/io/ImportData.hh" #include "celeritas/mat/MaterialParams.hh" #include "celeritas/optical/MaterialParams.hh" #include "celeritas/optical/ModelImporter.hh" #include "celeritas/optical/PhysicsParams.hh" #include "celeritas/optical/SimParams.hh" +#include "celeritas/optical/detector/ScoringParams.hh" #include "celeritas/optical/gen/CherenkovParams.hh" #include "celeritas/optical/gen/ScintillationParams.hh" #include "celeritas/optical/surface/SurfacePhysicsParams.hh" @@ -182,9 +184,8 @@ auto ImportedDataTestBase::build_optical_physics() -> SPConstOpticalPhysics //---------------------------------------------------------------------------// auto ImportedDataTestBase::build_optical_scoring() -> SPConstOpticalScoring { - // Optical scoring is just a user callback for optical hits, which isn't - // directly supported from import data. - return nullptr; + return std::make_shared( + this->optical_action_reg().get(), inp::OpticalScoring{}); } //---------------------------------------------------------------------------// diff --git a/test/celeritas/optical/Detector.test.cc b/test/celeritas/optical/Detector.test.cc index 4a420a714d..65ec6193b8 100644 --- a/test/celeritas/optical/Detector.test.cc +++ b/test/celeritas/optical/Detector.test.cc @@ -33,6 +33,14 @@ namespace test { using namespace ::celeritas::test; //---------------------------------------------------------------------------// +/*! + * Test optical detector and scoring. + * + * Because detectors are not directly loaded from GDML files, an override is + * used for loading the detectors into the core parameters. The default optical + * surface is set to be strictly transmitting to ensure hits are always + * recorded. + */ class DetectorTest : public ::celeritas::test::GeantTestBase { public: @@ -119,6 +127,12 @@ class DetectorTest : public ::celeritas::test::GeantTestBase inp::OpticalScoring scoring_input_; }; +//---------------------------------------------------------------------------// +// TESTS +//---------------------------------------------------------------------------// +// Run test to check small number of photons and hits to ensure correct hit +// information is populated. + struct SimpleScores { std::vector detector_ids; @@ -155,6 +169,8 @@ TEST_F(DetectorTest, simple) SimpleScores scores; scoring_input_.detector_callback = SimpleScorer{scores}; + // Manually generate arbitrary photons aimed at different detectors + using E = units::MevEnergy; using TI = TrackInitializer; @@ -203,12 +219,16 @@ TEST_F(DetectorTest, simple) ImplVolumeId{0}}, }; + // Run test + auto generate = DirectGeneratorAction::make_and_insert(*this->optical_params()); this->initialize_run(); generate->insert(*state_, make_span(inits)); (*transport_)(*state_); + // Check results + real_type const flight_time = 1.66782047599076e-09; static size_type const expected_detector_ids[] = {1, 1, 2, 2, 1, 0}; @@ -236,6 +256,9 @@ TEST_F(DetectorTest, simple) EXPECT_VEC_EQ(expected_volume_instance_ids, scores.volume_instance_ids); } +//---------------------------------------------------------------------------// +// Run test over large number of photons to check buffering is done correctly. + struct StressScorer { std::vector& scores; @@ -259,22 +282,29 @@ struct StressScorer TEST_F(DetectorTest, stress) { + // 3 detectors: x, y, z std::vector hits(3, 0); size_type errored = 0; scoring_input_.detector_callback = StressScorer{hits, errored}; + // Isotropically generate photons + inp::OpticalPrimaryGenerator gen; gen.primaries = 8192; gen.energy = inp::MonoenergeticDistribution{1e-5}; gen.angle = inp::IsotropicDistribution{}; gen.shape = inp::PointDistribution{{0, 0, 0}}; + // Run test + auto generate = PrimaryGeneratorAction::make_and_insert( *this->optical_params(), std::move(gen)); this->initialize_run(); generate->insert(*state_); (*transport_)(*state_); + // Check results + static size_type const expected_hits[] = {2673, 2816, 2703}; EXPECT_VEC_EQ(expected_hits, hits); diff --git a/test/celeritas/optical/SurfacePhysicsIntegrationTestBase.cc b/test/celeritas/optical/SurfacePhysicsIntegrationTestBase.cc index 5c07826106..e0ce21dbaf 100644 --- a/test/celeritas/optical/SurfacePhysicsIntegrationTestBase.cc +++ b/test/celeritas/optical/SurfacePhysicsIntegrationTestBase.cc @@ -51,24 +51,6 @@ auto SurfacePhysicsIntegrationTestBase::build_optical_surface_physics() this->setup_surface_models(input); - // // Default surface - - // PhysSurfaceId phys_surface = [&] { - // size_type num_surfaces = 0; - // for (auto const& mats : input.materials) - // { - // num_surfaces += mats.size() + 1; - // } - // return PhysSurfaceId(num_surfaces); - // }(); - - // input.materials.push_back({}); - // input.roughness.polished.emplace(phys_surface, inp::NoRoughness{}); - // input.reflectivity.fresnel.emplace(phys_surface, - // inp::FresnelReflection{}); - // input.interaction.trivial.emplace(phys_surface, - // TrivialInteractionMode::absorb); - return std::make_shared( this->optical_action_reg().get(), input); } From d109c1df2edd62fce844edd1301dfd6f9650b8ff Mon Sep 17 00:00:00 2001 From: Hayden Hollenbeck Date: Wed, 28 Jan 2026 17:10:00 -0600 Subject: [PATCH 07/11] Fix unused variable warning --- src/celeritas/optical/detector/DetectorData.hh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/celeritas/optical/detector/DetectorData.hh b/src/celeritas/optical/detector/DetectorData.hh index 4019c3bd8a..d6a9524d22 100644 --- a/src/celeritas/optical/detector/DetectorData.hh +++ b/src/celeritas/optical/detector/DetectorData.hh @@ -90,22 +90,22 @@ struct DetectorStateData template void copy_hits(DetectorHitOutput* output, DetectorStateData const& state, - StreamId); + StreamId stream); template<> -void copy_hits(DetectorHitOutput* output, +void copy_hits(DetectorHitOutput*, DetectorStateData const&, StreamId); template<> -void copy_hits(DetectorHitOutput* output, +void copy_hits(DetectorHitOutput*, DetectorStateData const&, StreamId); #if !CELER_USE_DEVICE template<> inline void -copy_hits(DetectorHitOutput* output, +copy_hits(DetectorHitOutput*, DetectorStateData const&, StreamId) { From df440ef3025428061d0349b8fd74b121c2636fa2 Mon Sep 17 00:00:00 2001 From: Hayden Hollenbeck Date: Wed, 28 Jan 2026 18:12:19 -0600 Subject: [PATCH 08/11] Fix error in copy_hits declaration --- .../optical/detector/DetectorData.cc | 2 +- .../optical/detector/DetectorData.cu | 7 ++-- .../optical/detector/DetectorData.hh | 15 +++++---- test/celeritas/optical/Detector.test.cc | 32 +++++++++++++------ 4 files changed, 36 insertions(+), 20 deletions(-) diff --git a/src/celeritas/optical/detector/DetectorData.cc b/src/celeritas/optical/detector/DetectorData.cc index 01272a94ce..a07650d3a6 100644 --- a/src/celeritas/optical/detector/DetectorData.cc +++ b/src/celeritas/optical/detector/DetectorData.cc @@ -20,7 +20,7 @@ namespace optical * the buffers. */ template<> -void copy_hits( +void copy_hits( DetectorHitOutput* output, DetectorStateData const& state, StreamId /* unused */) diff --git a/src/celeritas/optical/detector/DetectorData.cu b/src/celeritas/optical/detector/DetectorData.cu index 1955262a4e..f036709526 100644 --- a/src/celeritas/optical/detector/DetectorData.cu +++ b/src/celeritas/optical/detector/DetectorData.cu @@ -20,9 +20,10 @@ namespace optical * memory for only valid hits. */ template<> -copy_hits(DetectorHitOutput* output, - DetectorStateData const& state, - StreamId stream_id) +void copy_hits( + DetectorHitOutput* output, + DetectorStateData const& state, + StreamId stream_id) { CELER_EXPECT(output); CELER_EXPECT(stream_id); diff --git a/src/celeritas/optical/detector/DetectorData.hh b/src/celeritas/optical/detector/DetectorData.hh index d6a9524d22..572322f9fc 100644 --- a/src/celeritas/optical/detector/DetectorData.hh +++ b/src/celeritas/optical/detector/DetectorData.hh @@ -93,15 +93,18 @@ void copy_hits(DetectorHitOutput* output, StreamId stream); template<> -void copy_hits(DetectorHitOutput*, - DetectorStateData const&, - StreamId); +void copy_hits( + DetectorHitOutput*, + DetectorStateData const&, + StreamId); template<> -void copy_hits(DetectorHitOutput*, - DetectorStateData const&, - StreamId); +void copy_hits( + DetectorHitOutput*, + DetectorStateData const&, + StreamId); +//---------------------------------------------------------------------------// #if !CELER_USE_DEVICE template<> inline void diff --git a/test/celeritas/optical/Detector.test.cc b/test/celeritas/optical/Detector.test.cc index 65ec6193b8..d65fccb84f 100644 --- a/test/celeritas/optical/Detector.test.cc +++ b/test/celeritas/optical/Detector.test.cc @@ -32,6 +32,12 @@ namespace optical namespace test { using namespace ::celeritas::test; + +constexpr bool reference_configuration + = ((CELERITAS_REAL_TYPE == CELERITAS_REAL_TYPE_DOUBLE) + && !CELERITAS_VECGEOM_SURFACE + && CELERITAS_CORE_RNG == CELERITAS_CORE_RNG_XORWOW); + //---------------------------------------------------------------------------// /*! * Test optical detector and scoring. @@ -247,13 +253,16 @@ TEST_F(DetectorTest, simple) }; static size_type const expected_volume_instance_ids[] = {5, 4, 6, 7, 5, 3}; - EXPECT_VEC_EQ(expected_detector_ids, scores.detector_ids); - EXPECT_VEC_SOFT_EQ(expected_energies, scores.energies); - EXPECT_VEC_SOFT_EQ(expected_x_positions, scores.x_positions); - EXPECT_VEC_SOFT_EQ(expected_y_positions, scores.y_positions); - EXPECT_VEC_SOFT_EQ(expected_z_positions, scores.z_positions); - EXPECT_VEC_SOFT_EQ(expected_times, scores.times); - EXPECT_VEC_EQ(expected_volume_instance_ids, scores.volume_instance_ids); + if (reference_configuration) + { + EXPECT_VEC_EQ(expected_detector_ids, scores.detector_ids); + EXPECT_VEC_SOFT_EQ(expected_energies, scores.energies); + EXPECT_VEC_SOFT_EQ(expected_x_positions, scores.x_positions); + EXPECT_VEC_SOFT_EQ(expected_y_positions, scores.y_positions); + EXPECT_VEC_SOFT_EQ(expected_z_positions, scores.z_positions); + EXPECT_VEC_SOFT_EQ(expected_times, scores.times); + EXPECT_VEC_EQ(expected_volume_instance_ids, scores.volume_instance_ids); + } } //---------------------------------------------------------------------------// @@ -305,10 +314,13 @@ TEST_F(DetectorTest, stress) // Check results - static size_type const expected_hits[] = {2673, 2816, 2703}; + if (reference_configuration) + { + static size_type const expected_hits[] = {2673, 2816, 2703}; - EXPECT_VEC_EQ(expected_hits, hits); - EXPECT_EQ(errored, 0); + EXPECT_VEC_EQ(expected_hits, hits); + EXPECT_EQ(errored, 0); + } } //---------------------------------------------------------------------------// From d017443027260e46c572e3fde9ceeac7fbcd4aa1 Mon Sep 17 00:00:00 2001 From: Hayden Hollenbeck Date: Wed, 28 Jan 2026 18:23:26 -0600 Subject: [PATCH 09/11] Fix templating in DetectorAction to forward to correct host code calls --- .../optical/detector/DetectorAction.cc | 55 +++++++++++++++++++ .../optical/detector/DetectorAction.hh | 46 +++------------- 2 files changed, 62 insertions(+), 39 deletions(-) diff --git a/src/celeritas/optical/detector/DetectorAction.cc b/src/celeritas/optical/detector/DetectorAction.cc index 30914f7648..de5cb3f700 100644 --- a/src/celeritas/optical/detector/DetectorAction.cc +++ b/src/celeritas/optical/detector/DetectorAction.cc @@ -44,6 +44,61 @@ void DetectorAction::step(CoreParams const&, CoreStateDevice&) const } #endif +//---------------------------------------------------------------------------// +/*! + * Process hits copied from the kernels and send them to the callback. + */ +void DetectorAction::process_hits(CoreParams const& params, + CoreStateHost& state) const +{ + this->process_hits_impl(params, state); +} + +//---------------------------------------------------------------------------// +/*! + * Process hits copied from the kernels and send them to the callback. + */ +void DetectorAction::process_hits(CoreParams const& params, + CoreStateDevice& state) const +{ + this->process_hits_impl(params, state); +} + +//---------------------------------------------------------------------------// +/*! + * Process hits copied from the kernels and send them to the callback. + * + * Copied hits might be invalid, and are removed before sending into the + * callback function. The callback is only execute when a non-zero amount of + * valid hits occurs. + */ +template +void DetectorAction::process_hits_impl(CoreParams const& params, + CoreState& state) const +{ + DetectorHitOutput hit_results; + + // Copy hits (possibly from device) into pinned vector + copy_hits(&hit_results, state.ref().scoring, state.stream_id()); + + // Erase all hits with invalid detector ID + hit_results.hits.erase( + std::remove_if(hit_results.hits.begin(), + hit_results.hits.end(), + [](DetectorHit const& hit) { + return !static_cast(hit.detector); + }), + hit_results.hits.end()); + + // Send hits to the callback function, if there are any + if (!hit_results.hits.empty()) + { + auto scoring = params.scoring(); + CELER_ASSERT(scoring); + scoring->process_hits(make_span(hit_results.hits)); + } +} + //---------------------------------------------------------------------------// } // namespace optical } // namespace celeritas diff --git a/src/celeritas/optical/detector/DetectorAction.hh b/src/celeritas/optical/detector/DetectorAction.hh index 7a28b9a121..24f6936c11 100644 --- a/src/celeritas/optical/detector/DetectorAction.hh +++ b/src/celeritas/optical/detector/DetectorAction.hh @@ -47,48 +47,16 @@ class DetectorAction final : public OpticalStepActionInterface, StepActionOrder order() const final { return StepActionOrder::post; } private: - // Process hits copied from the kernels and send them to the callback + //!@{ + //! Process hits copied from the kernels and send them to the callback + void process_hits(CoreParams const&, CoreStateHost&) const; + void process_hits(CoreParams const&, CoreStateDevice&) const; + template - void process_hits(CoreParams const&, CoreState&) const; + void process_hits_impl(CoreParams const&, CoreState&) const; + //!@} }; -//---------------------------------------------------------------------------// -// INLINE DEFINITIONS -//---------------------------------------------------------------------------// -/*! - * Process hits copied from the kernels and send them to the callback. - * - * Copied hits might be invalid, and are removed before sending into the - * callback function. The callback is only execute when a non-zero amount of - * valid hits occurs. - */ -template -void DetectorAction::process_hits(CoreParams const& params, - CoreState& state) const -{ - DetectorHitOutput hit_results; - - // Copy hits (possibly from device) into pinned vector - copy_hits(&hit_results, state.ref().scoring, state.stream_id()); - - // Erase all hits with invalid detector ID - hit_results.hits.erase( - std::remove_if(hit_results.hits.begin(), - hit_results.hits.end(), - [](DetectorHit const& hit) { - return !static_cast(hit.detector); - }), - hit_results.hits.end()); - - // Send hits to the callback function, if there are any - if (!hit_results.hits.empty()) - { - auto scoring = params.scoring(); - CELER_ASSERT(scoring); - scoring->process_hits(make_span(hit_results.hits)); - } -} - //---------------------------------------------------------------------------// } // namespace optical } // namespace celeritas From 3152b48a721b69583212912caae7bf805b8f1f15 Mon Sep 17 00:00:00 2001 From: Hayden Hollenbeck Date: Wed, 28 Jan 2026 18:57:27 -0600 Subject: [PATCH 10/11] Use correct units in detector tests --- src/celeritas/optical/detector/DetectorAction.cc | 2 +- test/celeritas/optical/Detector.test.cc | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/celeritas/optical/detector/DetectorAction.cc b/src/celeritas/optical/detector/DetectorAction.cc index de5cb3f700..4ecf40d387 100644 --- a/src/celeritas/optical/detector/DetectorAction.cc +++ b/src/celeritas/optical/detector/DetectorAction.cc @@ -93,7 +93,7 @@ void DetectorAction::process_hits_impl(CoreParams const& params, // Send hits to the callback function, if there are any if (!hit_results.hits.empty()) { - auto scoring = params.scoring(); + auto const& scoring = params.scoring(); CELER_ASSERT(scoring); scoring->process_hits(make_span(hit_results.hits)); } diff --git a/test/celeritas/optical/Detector.test.cc b/test/celeritas/optical/Detector.test.cc index d65fccb84f..a70dd0349e 100644 --- a/test/celeritas/optical/Detector.test.cc +++ b/test/celeritas/optical/Detector.test.cc @@ -235,7 +235,8 @@ TEST_F(DetectorTest, simple) // Check results - real_type const flight_time = 1.66782047599076e-09; + real_type const box_size = from_cm(50); + real_type const flight_time = box_size / constants::c_light; static size_type const expected_detector_ids[] = {1, 1, 2, 2, 1, 0}; static real_type const expected_energies[] From 9300f46780734296584ff1c481d5c7b9e551f7cc Mon Sep 17 00:00:00 2001 From: Hayden Hollenbeck Date: Wed, 28 Jan 2026 19:15:37 -0600 Subject: [PATCH 11/11] Actually fix units in detector test --- test/celeritas/optical/Detector.test.cc | 27 ++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/test/celeritas/optical/Detector.test.cc b/test/celeritas/optical/Detector.test.cc index a70dd0349e..caf85a8da4 100644 --- a/test/celeritas/optical/Detector.test.cc +++ b/test/celeritas/optical/Detector.test.cc @@ -241,9 +241,30 @@ TEST_F(DetectorTest, simple) static size_type const expected_detector_ids[] = {1, 1, 2, 2, 1, 0}; static real_type const expected_energies[] = {1e-6, 2e-6, 3e-6, 4e-6, 5e-6, 6e-6}; - static real_type const expected_x_positions[] = {50, -50, 0, 0, 50, 0}; - static real_type const expected_y_positions[] = {0, 0, 0, 0, 0, -50}; - static real_type const expected_z_positions[] = {0, 0, 50, -50, 0, 0}; + static real_type const expected_x_positions[] = { + box_size, + -box_size, + 0, + 0, + box_size, + 0, + }; + static real_type const expected_y_positions[] = { + 0, + 0, + 0, + 0, + 0, + -box_size, + }; + static real_type const expected_z_positions[] = { + 0, + 0, + box_size, + -box_size, + 0, + 0, + }; static real_type const expected_times[] = { 0 + flight_time, 10 + flight_time,