From f8a3259a25708499e0b118c74b5dc736a588a510 Mon Sep 17 00:00:00 2001 From: "Seth R. Johnson" Date: Tue, 3 Dec 2024 08:03:29 -0500 Subject: [PATCH 01/15] Trim export data for unit tests (#1527) * Remove/deprecate ImportSBTable * Add import data trimmer * Trim data * Use references instead of pointers, trim livermore data * Update style guide --- app/celer-export-geant.cc | 35 +- doc/development/style.rst | 28 +- src/celeritas/CMakeLists.txt | 1 + src/celeritas/ext/RootInterfaceLinkDef.h | 1 - src/celeritas/io/ImportData.cc | 4 +- src/celeritas/io/ImportData.hh | 3 +- src/celeritas/io/ImportDataTrimmer.cc | 376 ++++++++++++++++++++++ src/celeritas/io/ImportDataTrimmer.hh | 97 ++++++ src/celeritas/io/ImportOpticalMaterial.hh | 3 +- src/celeritas/io/ImportSBTable.hh | 3 + test/celeritas/ext/RootJsonDumper.test.cc | 137 ++------ 11 files changed, 546 insertions(+), 142 deletions(-) create mode 100644 src/celeritas/io/ImportDataTrimmer.cc create mode 100644 src/celeritas/io/ImportDataTrimmer.hh diff --git a/app/celer-export-geant.cc b/app/celer-export-geant.cc index dc1c4ace8d..567af14440 100644 --- a/app/celer-export-geant.cc +++ b/app/celer-export-geant.cc @@ -28,6 +28,7 @@ #include "celeritas/ext/RootExporter.hh" #include "celeritas/ext/RootJsonDumper.hh" #include "celeritas/ext/ScopedRootErrorHandler.hh" +#include "celeritas/io/ImportDataTrimmer.hh" namespace celeritas { @@ -82,8 +83,15 @@ GeantPhysicsOptions load_options(std::string const& option_filename) void run(std::string const& gdml_filename, std::string const& opts_filename, std::string const& out_filename, - GeantImporter::DataSelection selection) + bool gen_test) { + // TODO: expose data selection to JSON users? + GeantImporter::DataSelection selection; + selection.particles = GeantImporter::DataSelection::em + | GeantImporter::DataSelection::optical; + selection.processes = selection.particles; + selection.reader_data = !gen_test; + // Construct options, set up Geant4, read data auto imported = [&] { GeantImporter import( @@ -91,8 +99,22 @@ void run(std::string const& gdml_filename, return import(selection); }(); + // TODO: expose trim data rather than bool 'gen_test' + if (gen_test) + { + ImportDataTrimmer::Input options; + options.mupp = true; + ImportDataTrimmer trim(options); + trim(imported); + } + ScopedRootErrorHandler scoped_root_error; + if (gen_test) + { + CELER_LOG(info) << "Trimming data for testing"; + } + if (ends_with(out_filename, ".root")) { // Write ROOT file @@ -164,17 +186,12 @@ int main(int argc, char* argv[]) return 2; } - GeantImporter::DataSelection selection; - selection.particles = GeantImporter::DataSelection::em - | GeantImporter::DataSelection::optical; - selection.processes = selection.particles; - selection.reader_data = true; - + bool gen_test{false}; if (args.size() == 4) { if (args.back() == "--gen-test") { - selection.reader_data = false; + gen_test = true; } else { @@ -186,7 +203,7 @@ int main(int argc, char* argv[]) try { - run(args[0], args[1], args[2], selection); + run(args[0], args[1], args[2], gen_test); } catch (RuntimeError const& e) { diff --git a/doc/development/style.rst b/doc/development/style.rst index 8e3d83f061..0bef151024 100644 --- a/doc/development/style.rst +++ b/doc/development/style.rst @@ -177,8 +177,11 @@ underscores). Prefer enumerations to boolean values in function interfaces definition to understand). -Function arguments and return values ------------------------------------- +Function inputs and outputs +--------------------------- + +The following rules are mostly derived from the `Google style guide`_, so refer +to that reference if not specified here. - Always pass value types for arguments when the data is small (``sizeof(arg) <= sizeof(void*)``). Using values instead of pointers/references allows the @@ -186,24 +189,25 @@ Function arguments and return values make a local copy anyway, it's OK to make the function argument a value (and use ``std::move`` internally as needed, but this is a more complicated topic). -- In general, avoid ``const`` values (e.g. ``const int``), because the decision - to modify a local variable or not is an implementation detail of the - function, not part of its interface. - Use const *references* for types that are nontrivial and that you only need to access or pass to other const-reference functions. -- Prefer return values or structs rather than mutable function arguments. This +- Prefer return values or structs rather return-by-reference. This makes it clear that there are no preconditions on the input value's state. -- In Celeritas we use the google style of passing mutable pointers instead of - mutable references, so that it's more obvious to the calling code that a - value is going to be modified. Add ``CELER_EXPECT(input);`` to make it clear - that the pointer needs to be valid, and add any other preconditions. +- In Celeritas we *formerly* used the google style of passing mutable pointers + instead of mutable references, so that it's more obvious to the calling code + that a value is going to be modified. The Google style changed and this has + fallen out of favor; **USE REFERENCES** except for the very rare case of + *optional* return values. - Host-only (e.g., runtime setup) code should almost never return raw pointers; use shared pointers instead to make the ownership semantics clear. When interfacing with older libraries such as Geant4, try to use ``unique_ptr`` and its ``release``/``get`` semantics to indicate the transfer of pointer ownership. -- Since we don't yet support C++17's ``string_view`` it's OK to use ``const - char*`` to indicate a read-only string. +- Avoid ``const`` *values* (e.g. ``const int``), because the decision + to modify a local variable or not is an implementation detail of the + function, not part of its interface. Clang-tidy will warn about this. + +.. _Google style guide: https://google.github.io/styleguide/cppguide.html#Inputs_and_Outputs Memory is always managed from host code, since on-device data management can be tricky, proprietary, and inefficient. There are no shared or unique pointers, diff --git a/src/celeritas/CMakeLists.txt b/src/celeritas/CMakeLists.txt index e19f2f86f3..73497a7a69 100644 --- a/src/celeritas/CMakeLists.txt +++ b/src/celeritas/CMakeLists.txt @@ -57,6 +57,7 @@ list(APPEND SOURCES grid/ValueGridType.cc io/AtomicRelaxationReader.cc io/ImportData.cc + io/ImportDataTrimmer.cc io/ImportMaterial.cc io/ImportModel.cc io/ImportOpticalModel.cc diff --git a/src/celeritas/ext/RootInterfaceLinkDef.h b/src/celeritas/ext/RootInterfaceLinkDef.h index 8366f48241..a8328aa06f 100644 --- a/src/celeritas/ext/RootInterfaceLinkDef.h +++ b/src/celeritas/ext/RootInterfaceLinkDef.h @@ -44,7 +44,6 @@ #pragma link C++ class celeritas::ImportProcess+; #pragma link C++ class celeritas::ImportProductionCut+; #pragma link C++ class celeritas::ImportRegion+; -#pragma link C++ class celeritas::ImportSBTable+; #pragma link C++ class celeritas::ImportScintComponent+; #pragma link C++ class celeritas::ImportScintData+; #pragma link C++ class celeritas::ImportTransParameters+; diff --git a/src/celeritas/io/ImportData.cc b/src/celeritas/io/ImportData.cc index 928ff3a966..67a44c285f 100644 --- a/src/celeritas/io/ImportData.cc +++ b/src/celeritas/io/ImportData.cc @@ -28,8 +28,8 @@ void convert_to_native(ImportData* data) // Convert data if (data->units.empty()) { - CELER_LOG(warning) << "Unit system missing from import data: assuming " - "CGS"; + CELER_LOG(warning) + << R"(Unit system missing from import data: assuming CGS)"; data->units = to_cstring(UnitSystem::cgs); } diff --git a/src/celeritas/io/ImportData.hh b/src/celeritas/io/ImportData.hh index 13f378655e..652b3dd282 100644 --- a/src/celeritas/io/ImportData.hh +++ b/src/celeritas/io/ImportData.hh @@ -20,7 +20,6 @@ #include "ImportParameters.hh" #include "ImportParticle.hh" #include "ImportProcess.hh" -#include "ImportSBTable.hh" #include "ImportVolume.hh" // IWYU pragma: end_exports @@ -58,7 +57,7 @@ struct ImportData //! \name Type aliases using ZInt = int; using GeoMatIndex = unsigned int; - using ImportSBMap = std::map; + using ImportSBMap = std::map; using ImportLivermorePEMap = std::map; using ImportAtomicRelaxationMap = std::map; using ImportNeutronElasticMap = std::map; diff --git a/src/celeritas/io/ImportDataTrimmer.cc b/src/celeritas/io/ImportDataTrimmer.cc new file mode 100644 index 0000000000..c77c857a29 --- /dev/null +++ b/src/celeritas/io/ImportDataTrimmer.cc @@ -0,0 +1,376 @@ +//----------------------------------*-C++-*----------------------------------// +// Copyright 2024 UT-Battelle, LLC, and other Celeritas developers. +// See the top-level COPYRIGHT file for details. +// SPDX-License-Identifier: (Apache-2.0 OR MIT) +//---------------------------------------------------------------------------// +//! \file celeritas/io/ImportDataTrimmer.cc +//---------------------------------------------------------------------------// +#include "ImportDataTrimmer.hh" + +#include +#include + +#include "corecel/Assert.hh" +#include "corecel/cont/Range.hh" + +namespace celeritas +{ +//---------------------------------------------------------------------------// +struct ImportDataTrimmer::GridFilterer +{ + size_type stride; + size_type orig_size; + + // Whether to keep the data at this index + inline bool operator()(size_type i) const; +}; + +//---------------------------------------------------------------------------// +/*! + * Construct from input parameters. + */ +ImportDataTrimmer::ImportDataTrimmer(Input const& inp) : options_{inp} +{ + CELER_EXPECT(options_.max_size >= 2); +} + +//---------------------------------------------------------------------------// +void ImportDataTrimmer::operator()(ImportData& data) +{ + if (options_.materials) + { + // Reduce the number of materials, elements, etc. + (*this)(data.isotopes); + (*this)(data.elements); + (*this)(data.geo_materials); + (*this)(data.phys_materials); + + (*this)(data.regions); + (*this)(data.volumes); + + (*this)(data.sb_data); + (*this)(data.livermore_pe_data); + (*this)(data.neutron_elastic_data); + (*this)(data.atomic_relaxation_data); + + (*this)(data.optical_materials); + } + + if (options_.physics) + { + // Reduce the number of physics processes + (*this)(data.particles); + (*this)(data.processes); + (*this)(data.msc_models); + } + + if (options_.mupp) + { + // Reduce the resolution of the muon pair production table + (*this)(data.mu_pair_production_data); + } + + this->for_each(data.elements); + this->for_each(data.geo_materials); + this->for_each(data.phys_materials); + + this->for_each(data.processes); + this->for_each(data.msc_models); + this->for_each(data.sb_data); + this->for_each(data.livermore_pe_data); + this->for_each(data.neutron_elastic_data); + this->for_each(data.atomic_relaxation_data); + + this->for_each(data.optical_models); + this->for_each(data.optical_materials); +} + +//---------------------------------------------------------------------------// +void ImportDataTrimmer::operator()(ImportElement& data) +{ + if (options_.materials) + { + (*this)(data.isotopes_fractions); + } +} + +//---------------------------------------------------------------------------// +void ImportDataTrimmer::operator()(ImportGeoMaterial& data) +{ + if (options_.materials) + { + (*this)(data.elements); + } +} + +//---------------------------------------------------------------------------// +void ImportDataTrimmer::operator()(ImportPhysMaterial&) +{ + // TODO: remap IDs? +} + +//---------------------------------------------------------------------------// +void ImportDataTrimmer::operator()(ImportOpticalMaterial& data) +{ + if (options_.physics) + { + (*this)(data.properties.refractive_index); + // TODO: trim WLS components? + } +} + +//---------------------------------------------------------------------------// +void ImportDataTrimmer::operator()(ImportOpticalModel& data) +{ + if (options_.materials) + { + (*this)(data.mfp_table); + } + + this->for_each(data.mfp_table); +} + +//---------------------------------------------------------------------------// +void ImportDataTrimmer::operator()(ImportModelMaterial& data) +{ + (*this)(data.energy); + + if (options_.materials) + { + (*this)(data.micro_xs); + } + + this->for_each(data.micro_xs); +} + +//---------------------------------------------------------------------------// +void ImportDataTrimmer::operator()(ImportModel& data) +{ + if (options_.materials) + { + (*this)(data.materials); + } + + this->for_each(data.materials); +} + +//---------------------------------------------------------------------------// +void ImportDataTrimmer::operator()(ImportMscModel& data) +{ + (*this)(data.xs_table); +} + +//---------------------------------------------------------------------------// +void ImportDataTrimmer::operator()(ImportMuPairProductionTable& data) +{ + if (!data) + { + return; + } + + (*this)(data.atomic_number); + (*this)(data.physics_vectors); + + this->for_each(data.physics_vectors); + + CELER_ENSURE(data); +} + +//---------------------------------------------------------------------------// +void ImportDataTrimmer::operator()(ImportLivermorePE& data) +{ + if (options_.physics) + { + (*this)(data.xs_lo); + (*this)(data.xs_hi); + (*this)(data.shells); + + this->for_each(data.shells); + } +} + +//---------------------------------------------------------------------------// +void ImportDataTrimmer::operator()(ImportLivermoreSubshell& data) +{ + if (options_.physics) + { + (*this)(data.param_lo); + (*this)(data.param_hi); + (*this)(data.xs); + (*this)(data.energy); + } +} + +//---------------------------------------------------------------------------// +void ImportDataTrimmer::operator()(ImportAtomicRelaxation&) +{ + // TODO: reduce shells/transitions +} + +//---------------------------------------------------------------------------// +void ImportDataTrimmer::operator()(ImportParticle&) {} + +//---------------------------------------------------------------------------// +void ImportDataTrimmer::operator()(ImportProcess& data) +{ + if (options_.materials) + { + (*this)(data.tables); + } + if (options_.physics) + { + (*this)(data.models); + } + + this->for_each(data.models); + this->for_each(data.tables); + + CELER_ENSURE(data); +} + +//---------------------------------------------------------------------------// +void ImportDataTrimmer::operator()(ImportPhysicsVector& data) +{ + (*this)(data.x); + (*this)(data.y); +} + +//---------------------------------------------------------------------------// +void ImportDataTrimmer::operator()(ImportPhysicsTable& data) +{ + if (options_.materials) + { + (*this)(data.physics_vectors); + } + + this->for_each(data.physics_vectors); +} + +//---------------------------------------------------------------------------// +void ImportDataTrimmer::operator()(ImportPhysics2DVector& data) +{ + auto x_filter = this->make_filterer(data.x.size()); + auto y_filter = this->make_filterer(data.y.size()); + + // Trim x and y grid + (*this)(data.x); + (*this)(data.y); + + std::vector new_value; + new_value.reserve(data.x.size() & data.y.size()); + + auto src = data.value.cbegin(); + for (auto i : range(x_filter.orig_size)) + { + for (auto j : range(y_filter.orig_size)) + { + if (x_filter(i) && y_filter(j)) + { + new_value.push_back(*src); + } + ++src; + } + } + CELER_ASSERT(src == data.value.cend()); + + data.value = std::move(new_value); + + CELER_ENSURE(data); +} + +//---------------------------------------------------------------------------// +/*! + * Trim the number of elements in a vector. + */ +template +void ImportDataTrimmer::operator()(std::vector& data) +{ + if (options_.max_size == numeric_limits::max()) + { + // Don't trim + return; + } + + std::vector result; + result.reserve(std::min(options_.max_size + 1, data.size())); + + auto filter = this->make_filterer(data.size()); + for (auto i : range(data.size())) + { + if (filter(i)) + { + result.push_back(data[i]); + } + } + data = std::move(result); +} + +//---------------------------------------------------------------------------// +/*! + * Trim the number of elements in a map. + */ +template +void ImportDataTrimmer::operator()(std::map& data) +{ + std::map result; + auto filter = this->make_filterer(data.size()); + auto iter = data.begin(); + for (auto i : range(filter.orig_size)) + { + auto cur_iter = iter++; + if (filter(i)) + { + result.insert(data.extract(cur_iter)); + } + } + data = std::move(result); +} + +//---------------------------------------------------------------------------// +/*! + * Trim each element in a vector. + */ +template +void ImportDataTrimmer::for_each(std::vector& data) +{ + for (auto& v : data) + { + (*this)(v); + } +} + +//---------------------------------------------------------------------------// +/*! + * Trim each value in a map. + */ +template +void ImportDataTrimmer::for_each(std::map& data) +{ + for (auto&& kv : data) + { + (*this)(kv.second); + } +} + +//---------------------------------------------------------------------------// +/*! + * Construct a filtering object. + */ +auto ImportDataTrimmer::make_filterer(size_type vec_size) const -> GridFilterer +{ + auto stride = vec_size >= 2 ? vec_size / (options_.max_size - 1) : 1; + + return {stride, vec_size}; +} + +//---------------------------------------------------------------------------// +/*! + * Whether to keep the data at this index. + */ +bool ImportDataTrimmer::GridFilterer::operator()(size_type i) const +{ + return i % stride == 0 || i + 1 == orig_size; +} + +//---------------------------------------------------------------------------// +} // namespace celeritas diff --git a/src/celeritas/io/ImportDataTrimmer.hh b/src/celeritas/io/ImportDataTrimmer.hh new file mode 100644 index 0000000000..123ef37608 --- /dev/null +++ b/src/celeritas/io/ImportDataTrimmer.hh @@ -0,0 +1,97 @@ +//----------------------------------*-C++-*----------------------------------// +// Copyright 2024 UT-Battelle, LLC, and other Celeritas developers. +// See the top-level COPYRIGHT file for details. +// SPDX-License-Identifier: (Apache-2.0 OR MIT) +//---------------------------------------------------------------------------// +//! \file celeritas/io/ImportDataTrimmer.hh +//---------------------------------------------------------------------------// +#pragma once + +#include +#include + +#include "corecel/math/NumericLimits.hh" + +#include "ImportData.hh" + +namespace celeritas +{ +//---------------------------------------------------------------------------// +/*! + * Reduce the amount of imported/exported data for testing. + */ +class ImportDataTrimmer +{ + public: + struct Input + { + //! Maximum number of elements in a vector (approximate) + std::size_t max_size{numeric_limits::max()}; + + //! Remove materials/elements which might affect the problem + bool materials{false}; + //! Reduce the number of physics models and processes + bool physics{false}; + //! Reduce the MuPPET table fidelity + bool mupp{false}; + }; + + public: + // Construct with a unit system + explicit ImportDataTrimmer(Input const& inp); + + //!@{ + //! Trim imported data + void operator()(ImportData& data); + void operator()(ImportElement& data); + void operator()(ImportGeoMaterial& data); + void operator()(ImportModel& data); + void operator()(ImportModelMaterial& data); + void operator()(ImportMscModel& data); + void operator()(ImportLivermorePE& data); + void operator()(ImportLivermoreSubshell& data); + void operator()(ImportAtomicRelaxation& data); + void operator()(ImportMuPairProductionTable& data); + void operator()(ImportOpticalMaterial& data); + void operator()(ImportOpticalModel& data); + void operator()(ImportParticle& data); + void operator()(ImportPhysMaterial& data); + void operator()(ImportProcess& data); + //!@} + + //!@{ + //! Trim objects + void operator()(ImportPhysicsVector& data); + void operator()(ImportPhysicsTable& data); + void operator()(ImportPhysics2DVector& data); + //!@} + + private: + //// TYPES //// + + using size_type = std::size_t; + struct GridFilterer; + + //// DATA //// + + Input options_; + + //// HELPERS //// + + GridFilterer make_filterer(size_type vec_size) const; + + template + void operator()(std::vector& vec); + + template + void operator()(std::map& m); + + template + void for_each(std::vector& vec); + + template + void for_each(std::map& m); +}; + +//---------------------------------------------------------------------------// +} // namespace celeritas diff --git a/src/celeritas/io/ImportOpticalMaterial.hh b/src/celeritas/io/ImportOpticalMaterial.hh index 8ce9c818a6..ac0959b62f 100644 --- a/src/celeritas/io/ImportOpticalMaterial.hh +++ b/src/celeritas/io/ImportOpticalMaterial.hh @@ -164,7 +164,8 @@ struct ImportWavelengthShift /*! * Store optical material properties. * - * \todo boolean for enabling cerenkov in the material?? + * \todo boolean for enabling cerenkov in the material?? DUNE e.g. disables + * cerenkov globally. */ struct ImportOpticalMaterial { diff --git a/src/celeritas/io/ImportSBTable.hh b/src/celeritas/io/ImportSBTable.hh index 741673fa9f..e26fbd2a32 100644 --- a/src/celeritas/io/ImportSBTable.hh +++ b/src/celeritas/io/ImportSBTable.hh @@ -4,6 +4,7 @@ // SPDX-License-Identifier: (Apache-2.0 OR MIT) //---------------------------------------------------------------------------// //! \file celeritas/io/ImportSBTable.hh +//! \deprecated Remove in v1.0 //---------------------------------------------------------------------------// #pragma once @@ -18,6 +19,8 @@ namespace celeritas * This 2-dimensional table stores the scaled bremsstrahlung differential cross * section [mb]. The x grid is the log energy of the incident particle [MeV], * and the y grid is the ratio of the gamma energy to the incident energy. + * + * DEPRECATED: remove in v1.0. */ using ImportSBTable = ImportPhysics2DVector; diff --git a/test/celeritas/ext/RootJsonDumper.test.cc b/test/celeritas/ext/RootJsonDumper.test.cc index cb4617a6c4..3f0ad37c4a 100644 --- a/test/celeritas/ext/RootJsonDumper.test.cc +++ b/test/celeritas/ext/RootJsonDumper.test.cc @@ -12,6 +12,7 @@ #include "celeritas/ext/RootImporter.hh" #include "celeritas/ext/ScopedRootErrorHandler.hh" #include "celeritas/io/ImportData.hh" +#include "celeritas/io/ImportDataTrimmer.hh" #include "celeritas_test.hh" @@ -19,87 +20,6 @@ namespace celeritas { namespace test { -//---------------------------------------------------------------------------// -template -void trim(T*); - -template -void trim(std::vector* vec); - -template -void trim(std::map* m); - -void trim(ImportModelMaterial* imm) -{ - CELER_EXPECT(imm); - trim(&imm->energy); - trim(&imm->micro_xs); -} - -void trim(ImportModel* im) -{ - CELER_EXPECT(im); - trim(&im->materials); -} - -void trim(ImportPhysicsVector* ipv) -{ - CELER_EXPECT(ipv); - trim(&ipv->x); - trim(&ipv->y); -} - -void trim(ImportPhysicsTable* ipt) -{ - CELER_EXPECT(ipt); - trim(&ipt->physics_vectors); -} - -void trim(ImportMscModel* im) -{ - CELER_EXPECT(im); - trim(&im->xs_table); -} - -void trim(ImportProcess* ip) -{ - CELER_EXPECT(ip); - trim(&ip->models); - trim(&ip->tables); -} - -template -void trim(T*) -{ - // Null-op by default -} - -template -void trim(std::vector* vec) -{ - CELER_EXPECT(vec); - if (vec->size() > 2) - { - vec->erase(vec->begin() + 1, vec->end() - 1); - } - for (auto& v : *vec) - { - trim(&v); - } -} - -template -void trim(std::map* m) -{ - CELER_EXPECT(m); - if (!m->empty()) - { - auto iter = m->begin(); - ++iter; - m->erase(iter, m->end()); - } -} - //---------------------------------------------------------------------------// class RootJsonDumperTest : public ::celeritas::test::Test { @@ -107,34 +27,33 @@ class RootJsonDumperTest : public ::celeritas::test::Test TEST_F(RootJsonDumperTest, all) { - ImportData imported; - { + // Import data + ImportData imported = [this] { ScopedRootErrorHandler scoped_root_error; RootImporter import( this->test_data_path("celeritas", "four-steel-slabs.root")); - imported = import(); + auto result = import(); scoped_root_error.throw_if_errors(); - } - - // Reduce amount of data to check... - trim(&imported.particles); - trim(&imported.isotopes); - trim(&imported.elements); - trim(&imported.geo_materials); - trim(&imported.phys_materials); - trim(&imported.processes); - trim(&imported.msc_models); - trim(&imported.regions); - trim(&imported.volumes); - imported.sb_data = {}; - imported.livermore_pe_data = {}; + return result; + }(); - std::ostringstream os; + // Trim data { + ImportDataTrimmer::Input inp; + inp.materials = true; + inp.physics = true; + inp.max_size = 2; + ImportDataTrimmer trim{inp}; + trim(imported); + } + + std::string str = [&imported] { + std::ostringstream os; ScopedRootErrorHandler scoped_root_error; RootJsonDumper{&os}(imported); scoped_root_error.throw_if_errors(); - } + return std::move(os).str(); + }(); if (CELERITAS_UNITS == CELERITAS_UNITS_CGS) { @@ -169,14 +88,6 @@ TEST_F(RootJsonDumperTest, all) "_typename" : "pair", "first" : 0, "second" : 0.05845 - }, { - "_typename" : "pair", - "first" : 1, - "second" : 0.91754 - }, { - "_typename" : "pair", - "first" : 2, - "second" : 0.02119 }, { "_typename" : "pair", "first" : 3, @@ -202,15 +113,11 @@ TEST_F(RootJsonDumperTest, all) "name" : "G4_STAINLESS-STEEL", "state" : 1, "temperature" : 293.15, - "number_density" : 86993489258991547580416, + "number_density" : 86993489258991530803200, "elements" : [{ "_typename" : "celeritas::ImportMatElemComponent", "element_id" : 0, "number_fraction" : 0.74 - }, { - "_typename" : "celeritas::ImportMatElemComponent", - "element_id" : 1, - "number_fraction" : 0.18 }, { "_typename" : "celeritas::ImportMatElemComponent", "element_id" : 2, @@ -387,7 +294,7 @@ TEST_F(RootJsonDumperTest, all) "_typename" : "celeritas::ImportPhysicsVector", "vector_type" : 2, "x" : [1e-4, 100], - "y" : [0.0919755519795959, 128.588033594672] + "y" : [0.0919755519795958, 128.588033594672] }] } }, { @@ -470,7 +377,7 @@ TEST_F(RootJsonDumperTest, all) }, "units" : "cgs" })json", - os.str()); + str); } } From 8e11b637848f848edbef7cbdb3b68d855e651f3b Mon Sep 17 00:00:00 2001 From: "Seth R. Johnson" Date: Tue, 3 Dec 2024 08:46:39 -0500 Subject: [PATCH 02/15] Add CITATION file with CHEP 2024 article (#1529) * Add citation file * Update format * Address reviewer feedback * Fix journal/title * Tweak citation --- CITATION.cff | 113 +++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 13 +++--- 2 files changed, 121 insertions(+), 5 deletions(-) create mode 100644 CITATION.cff diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000000..ddbd1b0cf2 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,113 @@ +# Pleace cite the following article when referencing Celeritas in a publication: +# +# (Plain text, Chicago author-date 17th ed): +# Johnson, Seth R., Amanda Lund, Philippe Canal, Stefano C. Tognini, Julien +# Esseiva, Soon Yung Jun, Guilherme Lima, et al. 2024. “Celeritas: +# Accelerating Geant4 with GPUs.” EPJ Web of Conferences 295:11005. +# https://doi.org/10.1051/epjconf/202429511005. +# +# (BibLaTeX): +# @article{celer-chep-2024, +# title = {Celeritas: {{Accelerating Geant4}} with {{GPUs}}}, +# author = {Johnson, Seth R. and Lund, Amanda and Canal, Philippe and Tognini, Stefano C. and Esseiva, Julien and Jun, Soon Yung and Lima, Guilherme and Biondo, Elliott and Evans, Thomas and Demarteau, Marcel and Romano, Paul}, +# date = {2024}, +# journaltitle = {EPJ Web of Conferences}, +# volume = {295}, +# pages = {11005}, +# doi = {10.1051/epjconf/202429511005} +# } +# +# (CFF): + +cff-version: 1.2.0 +type: software +message: "Please cite the following article when referencing Celeritas in a publication." +title: "Celeritas" +type: software +license: + - Apache-2.0 + - MIT +authors: + - family-names: Johnson + given-names: Seth R. + orcid: "https://orcid.org/0000-0003-1504-8966" + - family-names: Lund + given-names: Amanda + orcid: "https://orcid.org/0000-0002-8316-0709" + - family-names: Jun + given-names: Soon Yung + orcid: "https://orcid.org/0000-0003-3370-6109" + - family-names: Tognini + given-names: Stefano + orcid: "https://orcid.org/0000-0001-9741-6608" + - family-names: Lima + given-names: Guilherme + orcid: "https://orcid.org/0000-0003-4585-0546" + - family-names: Canal + given-names: Philippe + orcid: "https://orcid.org/0000-0002-7748-7887" + - family-names: Morgan + given-names: Ben + - family-names: Evans + given-names: Tom + orcid: "https://orcid.org/0000-0001-5743-3788" + - family-names: Esseiva + given-names: Julien +date-released: 2022 +url: https://doi.org/10.11578/dc.20221011.1 +identifiers: + - type: doi + value: 10.11578/dc.20221011.1 +keywords: + - monte carlo + - particle transport + - high energy physics + - detector simulation + - computational physics + - hep + - cuda + - hip +preferred-citation: + authors: + - family-names: Johnson + given-names: Seth R. + orcid: "https://orcid.org/0000-0003-1504-8966" + - family-names: Lund + given-names: Amanda + orcid: "https://orcid.org/0000-0002-8316-0709" + - family-names: Canal + given-names: Philippe + orcid: "https://orcid.org/0000-0002-7748-7887" + - family-names: Tognini + given-names: Stefano C. + orcid: "https://orcid.org/0000-0001-9741-6608" + - family-names: Esseiva + given-names: Julien + - family-names: Jun + given-names: Soon Yung + orcid: "https://orcid.org/0000-0003-3370-6109" + - family-names: Lima + given-names: Guilherme + orcid: "https://orcid.org/0000-0003-4585-0546" + - family-names: Biondo + given-names: Elliott + - family-names: Evans + given-names: Thomas + orcid: "https://orcid.org/0000-0001-5743-3788" + - family-names: Demarteau + given-names: Marcel + - family-names: Romano + given-names: Paul + orcid: "https://orcid.org/0000-0002-1147-045X" + title: "Celeritas: Accelerating Geant4 with GPUs" + type: article + journal: "EPJ Web of Conferences" + volume: 295 + number: 11005 + issue-title: "26th International Conference on Computing in High Energy and Nuclear Physics (CHEP 2023)" + date-published: 2024 + identifiers: + - type: doi + value: 10.1051/epjconf/202429511005 + abstract: >- + Celeritas is a new Monte Carlo (MC) detector simulation code designed for computationally intensive applications (specifically, High Luminosity Large Hadron Collider (HL-LHC) simulation) on high-performance heterogeneous architectures. In the past two years Celeritas has advanced from prototyping a GPU-based single physics model in infinite medium to implementing a full set of electromagnetic (EM) physics processes in complex geometries. The current release of Celeritas, version 0.3, has incorporated full device-based navigation, an event loop in the presence of magnetic fields, and detector hit scoring. New functionality incorporates a scheduler to offload electromagnetic physics to the GPU within a Geant4-driven simulation, enabling integration of Celeritas into high energy physics (HEP) experimental frameworks such as CMSSW. On the Summit supercomputer, Celeritas performs EM physics between 6× and 32× faster using the machine’s Nvidia GPUs compared to using only CPUs. When running a multithreaded Geant4 ATLAS test beam application with full hadronic physics, using Celeritas to accelerate the EM physics results in an overall simulation speedup of 1.8–2.3× on GPU and 1.2× on CPU. diff --git a/README.md b/README.md index b48e6e3a4d..26ba506add 100644 --- a/README.md +++ b/README.md @@ -124,17 +124,20 @@ details on coding in Celeritas, and [the administration guidelines][administrati | **doc** | Code documentation and manual | | **example** | Example applications and input files | | **external** | Automatically fetched external CMake dependencies | -| **interface** | Wrapper interfaces to Celeritas library functions | | **scripts** | Development and continuous integration helper scripts | | **src** | Library source code | | **test** | Unit tests | # Citing Celeritas -If using Celeritas in your work, we ask that you cite the code using its -[DOECode](https://www.osti.gov/doecode/biblio/94866) registration: +If using Celeritas in your work, we ask that you cite the following article: -> Seth R. Johnson, Amanda Lund, Soon Yung Jun, Stefano Tognini, Guilherme Lima, Paul Romano, Philippe Canal, Ben Morgan, and Tom Evans. “Celeritas,” July 2022. https://doi.org/10.11578/dc.20221011.1. +> Johnson, Seth R., Amanda Lund, Philippe Canal, Stefano C. Tognini, Julien Esseiva, Soon Yung Jun, Guilherme Lima, et al. 2024. “Celeritas: Accelerating Geant4 with GPUs.” EPJ Web of Conferences 295:11005. https://doi.org/10.1051/epjconf/202429511005. + +See also its [DOECode](https://www.osti.gov/doecode/biblio/94866) registration: + +> Johnson, Seth R., Amanda Lund, Soon Yung Jun, Stefano Tognini, Guilherme Lima, Philippe Canal, Ben Morgan, Tom Evans, and Julien Esseiva. 2022. “Celeritas.” https://doi.org/10.11578/dc.20221011.1. A continually evolving list of works authored by (or with content authored by) -core team members is available in our [citation file](doc/_static/celeritas.bib). +core team members is continually updated at [our publications page](https://github.com/celeritas-project/celeritas/blob/doc/gh-pages-base/publications.md) +and displayed on [the official project web site](https://celeritas.ornl.gov/). From a85f17cecd10fb20343f1426468740607ab71fac Mon Sep 17 00:00:00 2001 From: lebuller <65980408+lebuller@users.noreply.github.com> Date: Tue, 3 Dec 2024 14:53:34 -0500 Subject: [PATCH 03/15] Add option to use spline-interpolated energy loss (#1496) * Adds `spline_eloss_order` variable to the PhysicsParamsScalars and PhysicsParamsOptions * Adds option for xs calculation from spline in energy loss calculation * Adds spline order to the runner input setup and includes it as an input in the simple-driver.py * Set a default of 1st order interpolation in the input file * Adds spline physics step test. Currently fails due to the energy loss grid created being too small * Adds spline_calc_xs function for testing the make_calculator with a spline template. Failing due to energy grid of size 2 in test * Alter the vector length to store a range of values for the energy loss in MockProcess. Also alter scattering mock process to have 3 xs points instead of 2 --- app/celer-sim/Runner.cc | 1 + app/celer-sim/RunnerInput.hh | 1 + app/celer-sim/RunnerInputIO.json.cc | 2 + app/celer-sim/simple-driver.py | 2 + src/celeritas/phys/PhysicsData.hh | 7 +- src/celeritas/phys/PhysicsParams.cc | 4 ++ src/celeritas/phys/PhysicsParams.hh | 6 ++ src/celeritas/phys/PhysicsParamsOutput.cc | 1 + src/celeritas/phys/PhysicsStepUtils.hh | 19 ++++- src/celeritas/phys/PhysicsTrackView.hh | 20 ++++++ test/celeritas/MockTestBase.cc | 2 +- test/celeritas/phys/MockProcess.cc | 3 +- test/celeritas/phys/Physics.test.cc | 27 ++++++- test/celeritas/phys/PhysicsStepUtils.test.cc | 75 ++++++++++++++++++++ 14 files changed, 162 insertions(+), 8 deletions(-) diff --git a/app/celer-sim/Runner.cc b/app/celer-sim/Runner.cc index a1223640b7..5291e5f49c 100644 --- a/app/celer-sim/Runner.cc +++ b/app/celer-sim/Runner.cc @@ -351,6 +351,7 @@ void Runner::build_core_params(RunnerInput const& inp, input.options.linear_loss_limit = imported.em_params.linear_loss_limit; input.options.lowest_electron_energy = PhysicsParamsOptions::Energy( imported.em_params.lowest_electron_energy); + input.options.spline_eloss_order = inp.spline_eloss_order; input.processes = [¶ms, &inp, &imported] { std::vector> result; diff --git a/app/celer-sim/RunnerInput.hh b/app/celer-sim/RunnerInput.hh index 660bd49fee..a8a87035eb 100644 --- a/app/celer-sim/RunnerInput.hh +++ b/app/celer-sim/RunnerInput.hh @@ -105,6 +105,7 @@ struct RunnerInput size_type num_track_slots{}; //!< Divided among streams size_type max_steps = static_cast(-1); size_type initializer_capacity{}; //!< Divided among streams + size_type spline_eloss_order = 1; real_type secondary_stack_factor{}; bool use_device{}; bool action_times{}; diff --git a/app/celer-sim/RunnerInputIO.json.cc b/app/celer-sim/RunnerInputIO.json.cc index e4fdebe0e8..83536ad8c1 100644 --- a/app/celer-sim/RunnerInputIO.json.cc +++ b/app/celer-sim/RunnerInputIO.json.cc @@ -88,6 +88,7 @@ void from_json(nlohmann::json const& j, RunnerInput& v) LDIO_LOAD_OPTION(max_steps); LDIO_LOAD_REQUIRED(initializer_capacity); LDIO_LOAD_REQUIRED(secondary_stack_factor); + LDIO_LOAD_OPTION(spline_eloss_order); LDIO_LOAD_REQUIRED(use_device); LDIO_LOAD_OPTION(action_times); LDIO_LOAD_OPTION(merge_events); @@ -183,6 +184,7 @@ void to_json(nlohmann::json& j, RunnerInput const& v) LDIO_SAVE_OPTION(max_steps); LDIO_SAVE(initializer_capacity); LDIO_SAVE(secondary_stack_factor); + LDIO_SAVE_OPTION(spline_eloss_order); LDIO_SAVE(use_device); LDIO_SAVE(action_times); LDIO_SAVE(merge_events); diff --git a/app/celer-sim/simple-driver.py b/app/celer-sim/simple-driver.py index e776aefc3a..b25844583a 100755 --- a/app/celer-sim/simple-driver.py +++ b/app/celer-sim/simple-driver.py @@ -80,6 +80,7 @@ def strtobool(text): num_tracks = 128 * 32 if use_device else 32 num_primaries = 3 * 15 # assuming test hepmc input max_steps = 512 if physics_options['msc'] else 128 +spline_eloss_order = 2 if not use_device: # Way more steps are needed since we're not tracking in parallel, but @@ -103,6 +104,7 @@ def strtobool(text): 'seed': 12345, 'num_track_slots': num_tracks, 'max_steps': max_steps, + 'spline_eloss_order': spline_eloss_order, 'initializer_capacity': 100 * max([num_tracks, num_primaries]), 'secondary_stack_factor': 3, 'action_diagnostic': True, diff --git a/src/celeritas/phys/PhysicsData.hh b/src/celeritas/phys/PhysicsData.hh index d81d482927..797b0f3086 100644 --- a/src/celeritas/phys/PhysicsData.hh +++ b/src/celeritas/phys/PhysicsData.hh @@ -241,6 +241,9 @@ struct PhysicsParamsScalars real_type linear_loss_limit{}; //!< For scaled range calculation real_type fixed_step_limiter{}; //!< Global charged step size limit [len] + //! Order for energy loss interpolation + size_type spline_eloss_order{}; + // User-configurable multiple scattering options real_type lambda_limit{}; //!< lambda limit real_type range_factor{}; //!< range factor for e-/e+ (0.2 for muon/h) @@ -263,8 +266,8 @@ struct PhysicsParamsScalars && linear_loss_limit > 0 && secondary_stack_factor > 0 && ((fixed_step_limiter > 0) == static_cast(fixed_step_action)) - && lambda_limit > 0 && range_factor > 0 && range_factor < 1 - && safety_factor >= 0.1 + && spline_eloss_order > 0 && lambda_limit > 0 + && range_factor > 0 && range_factor < 1 && safety_factor >= 0.1 && step_limit_algorithm != MscStepLimitAlgorithm::size_; } diff --git a/src/celeritas/phys/PhysicsParams.cc b/src/celeritas/phys/PhysicsParams.cc index 65db096860..c91a3d3ac9 100644 --- a/src/celeritas/phys/PhysicsParams.cc +++ b/src/celeritas/phys/PhysicsParams.cc @@ -252,6 +252,9 @@ void PhysicsParams::build_options(Options const& opts, HostValue* data) const CELER_VALIDATE(opts.range_factor > 0 && opts.range_factor < 1, << "invalid range_factor=" << opts.range_factor << " (should be within 0 < limit < 1)"); + CELER_VALIDATE(opts.spline_eloss_order > 0, + << "invalid spline_eloss_order=" << opts.spline_eloss_order + << " (should be > 0)"); data->scalars.min_range = opts.min_range; data->scalars.max_step_over_range = opts.max_step_over_range; data->scalars.min_eprime_over_e = opts.min_eprime_over_e; @@ -262,6 +265,7 @@ void PhysicsParams::build_options(Options const& opts, HostValue* data) const data->scalars.range_factor = opts.range_factor; data->scalars.safety_factor = opts.safety_factor; data->scalars.step_limit_algorithm = opts.step_limit_algorithm; + data->scalars.spline_eloss_order = opts.spline_eloss_order; if (data->scalars.step_limit_algorithm == MscStepLimitAlgorithm::distance_to_boundary) { diff --git a/src/celeritas/phys/PhysicsParams.hh b/src/celeritas/phys/PhysicsParams.hh index ae954dbe38..c463350ffb 100644 --- a/src/celeritas/phys/PhysicsParams.hh +++ b/src/celeritas/phys/PhysicsParams.hh @@ -69,6 +69,10 @@ class ParticleParams; * processes use MC integration to sample the discrete interaction length * with the correct probability. Disable this integral approach for all * processes. + * \c spline_eloss_order: the order of interpolation to be used for the + * spline interpolation. If it is 1, then the existing linear interpolation + * is used. If it is 2+, the spline interpolation is used for energy loss + * using the specified order. Default value is 1 * * NOTE: min_range/max_step_over_range are not accessible through Geant4, and * they can also be set to be different for electrons, mu/hadrons, and ions @@ -102,6 +106,8 @@ struct PhysicsParamsOptions real_type secondary_stack_factor = 3; bool disable_integral_xs = false; + + size_type spline_eloss_order = 1; }; //---------------------------------------------------------------------------// diff --git a/src/celeritas/phys/PhysicsParamsOutput.cc b/src/celeritas/phys/PhysicsParamsOutput.cc index a749d8ec43..002dff6186 100644 --- a/src/celeritas/phys/PhysicsParamsOutput.cc +++ b/src/celeritas/phys/PhysicsParamsOutput.cc @@ -85,6 +85,7 @@ void PhysicsParamsOutput::output(JsonPimpl* j) const PPO_SAVE_OPTION(lowest_electron_energy); PPO_SAVE_OPTION(linear_loss_limit); PPO_SAVE_OPTION(fixed_step_limiter); + PPO_SAVE_OPTION(spline_eloss_order); #undef PPO_SAVE_OPTION obj["options"] = std::move(options); } diff --git a/src/celeritas/phys/PhysicsStepUtils.hh b/src/celeritas/phys/PhysicsStepUtils.hh index a73bb74b73..4120a955d9 100644 --- a/src/celeritas/phys/PhysicsStepUtils.hh +++ b/src/celeritas/phys/PhysicsStepUtils.hh @@ -17,6 +17,7 @@ #include "celeritas/grid/EnergyLossCalculator.hh" #include "celeritas/grid/InverseRangeCalculator.hh" #include "celeritas/grid/RangeCalculator.hh" +#include "celeritas/grid/SplineXsCalculator.hh" #include "celeritas/grid/ValueGridType.hh" #include "celeritas/grid/XsCalculator.hh" #include "celeritas/mat/MaterialTrackView.hh" @@ -194,9 +195,21 @@ calc_mean_energy_loss(ParticleTrackView const& particle, { auto grid_id = physics.value_grid(VGT::energy_loss, ppid); CELER_ASSERT(grid_id); - auto calc_eloss_rate - = physics.make_calculator(grid_id); - eloss = Energy{step * calc_eloss_rate(pre_step_energy)}; + + size_type order = physics.scalars().spline_eloss_order; + + if (order == 1) + { + auto calc_eloss_rate + = physics.make_calculator(grid_id); + eloss = Energy{step * calc_eloss_rate(pre_step_energy)}; + } + else + { + auto calc_eloss_rate + = physics.make_calculator(grid_id, order); + eloss = Energy{step * calc_eloss_rate(pre_step_energy)}; + } } if (eloss >= pre_step_energy * physics.scalars().linear_loss_limit) diff --git a/src/celeritas/phys/PhysicsTrackView.hh b/src/celeritas/phys/PhysicsTrackView.hh index edf0d5bc59..cdec923bba 100644 --- a/src/celeritas/phys/PhysicsTrackView.hh +++ b/src/celeritas/phys/PhysicsTrackView.hh @@ -153,6 +153,11 @@ class PhysicsTrackView template inline CELER_FUNCTION T make_calculator(ValueGridId) const; + // Construct a grid calculator from a physics table + template + inline CELER_FUNCTION T make_calculator(ValueGridId, + size_type order) const; + //// HACKS //// // Get hardwired model, null if not present @@ -672,6 +677,21 @@ CELER_FUNCTION T PhysicsTrackView::make_calculator(ValueGridId id) const return T{params_.value_grids[id], params_.reals}; } +//---------------------------------------------------------------------------// +/*! + * Construct a Spline grid calculator of the given type + * + * The calculator must take three arguments: a reference to XsGridRef, a + * reference to the Values data structure, and an interpolation order. + */ +template +CELER_FUNCTION T PhysicsTrackView::make_calculator(ValueGridId id, + size_type order) const +{ + CELER_EXPECT(id < params_.value_grids.size()); + return T{params_.value_grids[id], params_.reals, order}; +} + //---------------------------------------------------------------------------// // IMPLEMENTATION HELPER FUNCTIONS //---------------------------------------------------------------------------// diff --git a/test/celeritas/MockTestBase.cc b/test/celeritas/MockTestBase.cc index e70e86bf16..513d05fce1 100644 --- a/test/celeritas/MockTestBase.cc +++ b/test/celeritas/MockTestBase.cc @@ -161,7 +161,7 @@ auto MockTestBase::build_physics() -> SPConstPhysics inp.use_integral_xs = false; inp.applic = {make_applicability("gamma", 1e-6, 100), make_applicability("celeriton", 1, 100)}; - inp.xs = {Barn{1.0}, Barn{1.0}}; + inp.xs = {Barn{1.0}, Barn{1.0}, Barn{1.0}}; inp.energy_loss = {}; physics_inp.processes.push_back(std::make_shared(inp)); } diff --git a/test/celeritas/phys/MockProcess.cc b/test/celeritas/phys/MockProcess.cc index 05cd4b046e..2214da2303 100644 --- a/test/celeritas/phys/MockProcess.cc +++ b/test/celeritas/phys/MockProcess.cc @@ -78,11 +78,12 @@ auto MockProcess::step_limits(Applicability applic) const -> StepLimitBuilders { auto eloss_rate = native_value_to( native_value_from(data_.energy_loss) * numdens); + builders[ValueGridType::energy_loss] = std::make_unique( applic.lower.value(), applic.upper.value(), - VecDbl{eloss_rate.value(), eloss_rate.value()}); + VecDbl(3, eloss_rate.value())); builders[ValueGridType::range] = std::make_unique( applic.lower.value(), diff --git a/test/celeritas/phys/Physics.test.cc b/test/celeritas/phys/Physics.test.cc index 56dde440ad..afea841c62 100644 --- a/test/celeritas/phys/Physics.test.cc +++ b/test/celeritas/phys/Physics.test.cc @@ -16,6 +16,7 @@ #include "celeritas/em/process/EPlusAnnihilationProcess.hh" #include "celeritas/grid/EnergyLossCalculator.hh" #include "celeritas/grid/RangeCalculator.hh" +#include "celeritas/grid/SplineXsCalculator.hh" #include "celeritas/grid/XsCalculator.hh" #include "celeritas/mat/MaterialParams.hh" #include "celeritas/phys/ParticleParams.hh" @@ -141,7 +142,7 @@ TEST_F(PhysicsParamsTest, output) GTEST_SKIP() << "Test results are based on CGS units"; } EXPECT_JSON_EQ( - R"json({"_category":"internal","_label":"physics","models":{"label":["mock-model-1","mock-model-2","mock-model-3","mock-model-4","mock-model-5","mock-model-6","mock-model-7","mock-model-8","mock-model-9","mock-model-10","mock-model-11"],"process_id":[0,0,1,2,2,2,3,3,4,4,5]},"options":{"fixed_step_limiter":0.0,"linear_loss_limit":0.01,"lowest_electron_energy":[0.001,"MeV"],"max_step_over_range":0.2,"min_eprime_over_e":0.8,"min_range":0.1},"processes":{"label":["scattering","absorption","purrs","hisses","meows","barks"]},"sizes":{"integral_xs":8,"model_groups":8,"model_ids":11,"process_groups":5,"process_ids":8,"reals":231,"value_grid_ids":89,"value_grids":89,"value_tables":35}})json", + R"json({"_category":"internal","_label":"physics","models":{"label":["mock-model-1","mock-model-2","mock-model-3","mock-model-4","mock-model-5","mock-model-6","mock-model-7","mock-model-8","mock-model-9","mock-model-10","mock-model-11"],"process_id":[0,0,1,2,2,2,3,3,4,4,5]},"options":{"fixed_step_limiter":0.0,"linear_loss_limit":0.01,"lowest_electron_energy":[0.001,"MeV"],"max_step_over_range":0.2,"min_eprime_over_e":0.8,"min_range":0.1,"spline_eloss_order":1},"processes":{"label":["scattering","absorption","purrs","hisses","meows","barks"]},"sizes":{"integral_xs":8,"model_groups":8,"model_ids":11,"process_groups":5,"process_ids":8,"reals":257,"value_grid_ids":89,"value_grids":89,"value_tables":35}})json", to_string(out)); } @@ -648,6 +649,30 @@ TEST_F(PhysicsTrackViewHostTest, cuda_surrogate) EXPECT_VEC_SOFT_EQ(expected_step, step); } +TEST_F(PhysicsTrackViewHostTest, calc_spline_xs) +{ + // Cross sections: same across particle types, constant in energy, scale + // according to material number density + std::vector xs; + for (char const* particle : {"gamma", "celeriton"}) + { + for (auto mat_id : range(MaterialId{this->material()->size()})) + { + PhysicsTrackView const phys + = this->make_track_view(particle, mat_id); + auto scat_ppid = this->find_ppid(phys, "scattering"); + auto id = phys.value_grid(ValueGridType::macro_xs, scat_ppid); + ASSERT_TRUE(id); + auto calc_xs = phys.make_calculator(id, 2); + xs.push_back(to_inv_cm(calc_xs(MevEnergy{1.0}))); + } + } + + double const expected_xs[] + = {0.0001, 0.001, 0.1, 1e-24, 0.0001, 0.001, 0.1, 1e-24}; + EXPECT_VEC_SOFT_EQ(expected_xs, xs); +} + //---------------------------------------------------------------------------// // PHYSICS TRACK VIEW (DEVICE) //---------------------------------------------------------------------------// diff --git a/test/celeritas/phys/PhysicsStepUtils.test.cc b/test/celeritas/phys/PhysicsStepUtils.test.cc index 502f4caf87..188c70dbff 100644 --- a/test/celeritas/phys/PhysicsStepUtils.test.cc +++ b/test/celeritas/phys/PhysicsStepUtils.test.cc @@ -460,5 +460,80 @@ TEST_F(StepLimiterTest, calc_physics_step_limit) } } //---------------------------------------------------------------------------// + +class SplinePhysicsStepUtilsTest : public PhysicsStepUtilsTest +{ + PhysicsOptions build_physics_options() const override + { + PhysicsOptions opts; + opts.spline_eloss_order = 2; + return opts; + } +}; + +TEST_F(SplinePhysicsStepUtilsTest, calc_mean_energy_loss) +{ + MaterialTrackView material( + this->material()->host_ref(), mat_state.ref(), TrackSlotId{0}); + ParticleTrackView particle( + this->particle()->host_ref(), par_state.ref(), TrackSlotId{0}); + + // input: cm; output: MeV + auto calc_eloss = [&](PhysicsTrackView& phys, real_type step) -> real_type { + // Calculate and store the energy loss range to PhysicsTrackView + auto ppid = phys.eloss_ppid(); + auto grid_id = phys.value_grid(ValueGridType::range, ppid); + auto calc_range = phys.make_calculator(grid_id); + real_type range = calc_range(particle.energy()); + phys.dedx_range(range); + + auto result + = calc_mean_energy_loss(particle, phys, step * units::centimeter); + return value_as(result); + }; + + { + PhysicsTrackView phys = this->init_track( + &material, MaterialId{0}, &particle, "gamma", MevEnergy{1}); + if (CELERITAS_DEBUG) + { + // Can't calc eloss for photons + EXPECT_THROW(calc_eloss(phys, 1e4), DebugError); + } + } + { + PhysicsTrackView phys = this->init_track( + &material, MaterialId{0}, &particle, "celeriton", MevEnergy{10}); + real_type const eloss_rate = (0.2 + 0.4); // MeV / cm + + // Tiny step: should still be linear loss (single process) + EXPECT_SOFT_EQ(eloss_rate * 1e-6, calc_eloss(phys, 1e-6)); + + // Long step (lose half energy) will call inverse lookup. The correct + // answer (if range table construction was done over energy loss) + // should be half since the slowing down rate is constant over all + real_type step = 0.5 * particle.energy().value() / eloss_rate; + EXPECT_SOFT_EQ(5, calc_eloss(phys, step)); + + // Long step (lose half energy) will call inverse lookup. The correct + // answer (if range table construction was done over energy loss) + // should be half since the slowing down rate is constant over all + step = 0.999 * particle.energy().value() / eloss_rate; + EXPECT_SOFT_EQ(9.99, calc_eloss(phys, step)); + } + { + PhysicsTrackView phys = this->init_track( + &material, MaterialId{0}, &particle, "electron", MevEnergy{1e-3}); + real_type const eloss_rate = 0.5; // MeV / cm + + // Low energy particle which loses all its energy over the step will + // call inverse lookup. Remaining range will be zero and eloss will be + // equal to the pre-step energy. + real_type step = value_as(particle.energy()) / eloss_rate; + EXPECT_SOFT_EQ(1e-3, calc_eloss(phys, step)); + } +} +//---------------------------------------------------------------------------// + } // namespace test } // namespace celeritas From d2871473f447f27b97b43876258f87bb17c08746 Mon Sep 17 00:00:00 2001 From: lebuller <65980408+lebuller@users.noreply.github.com> Date: Tue, 3 Dec 2024 17:42:27 -0500 Subject: [PATCH 04/15] Add option to use spline-interpolated energy loss (#1496) * Adds `spline_eloss_order` variable to the PhysicsParamsScalars and PhysicsParamsOptions * Adds option for xs calculation from spline in energy loss calculation * Adds spline order to the runner input setup and includes it as an input in the simple-driver.py * Set a default of 1st order interpolation in the input file * Adds spline physics step test. Currently fails due to the energy loss grid created being too small * Adds spline_calc_xs function for testing the make_calculator with a spline template. Failing due to energy grid of size 2 in test * Alter the vector length to store a range of values for the energy loss in MockProcess. Also alter scattering mock process to have 3 xs points instead of 2 * Used size,fill constructor for eloss rate vector in MockProcess * Add save option to the runner input IO for the spline order * Reorder and reformat boolean function for PhysicsData * reindent comment for physicsParams.hh * in calc_mean_energy_loss, order has already been asserted > 0, so checks on that can be removed * remove const argument for spline order in make calculator From e0dc0a8959b595aebb14553076451aa5b3c96603 Mon Sep 17 00:00:00 2001 From: Soon Yung Jun Date: Tue, 3 Dec 2024 19:32:01 -0500 Subject: [PATCH 05/15] Add an Optical WLS (#1507) * Add WavelengthShift * Add a secondary photon store * Fix a warning from the window build: conversion from celeritas::real_type to int * Fix errors from unit and precision tests * Rename ScalarPropertyWLS to WlsMaterialRecord * Add a short description of WLS and changes for other review comments * Add a note on the secondary memory allocation * Add the unit for the expected average WLS time * Use consistent float types to fix `float` build error * Add todos * Fix no-WLS construction * Change the interaction action from_unchanged() to from_absorption() if there is no secondary photon generated from WLS * Fixed the test result when there is no emitted WLS photon --------- Co-authored-by: Soon Yung Jun Co-authored-by: Soon Yung Jun Co-authored-by: Soon Yung Jun Co-authored-by: Seth R Johnson Co-authored-by: Soon Yung Jun --- src/celeritas/CMakeLists.txt | 1 + src/celeritas/optical/CerenkovParams.cc | 1 + src/celeritas/optical/Interaction.hh | 18 ++ src/celeritas/optical/WavelengthShiftData.hh | 80 ++++++ .../optical/WavelengthShiftParams.cc | 108 ++++++++ .../optical/WavelengthShiftParams.hh | 58 +++++ .../interactor/WavelengthShiftInteractor.hh | 180 +++++++++++++ test/celeritas/CMakeLists.txt | 1 + .../optical/InteractorHostTestBase.cc | 15 ++ .../optical/InteractorHostTestBase.hh | 16 ++ .../celeritas/optical/WavelengthShift.test.cc | 244 ++++++++++++++++++ 11 files changed, 722 insertions(+) create mode 100644 src/celeritas/optical/WavelengthShiftData.hh create mode 100644 src/celeritas/optical/WavelengthShiftParams.cc create mode 100644 src/celeritas/optical/WavelengthShiftParams.hh create mode 100644 src/celeritas/optical/interactor/WavelengthShiftInteractor.hh create mode 100644 test/celeritas/optical/WavelengthShift.test.cc diff --git a/src/celeritas/CMakeLists.txt b/src/celeritas/CMakeLists.txt index 73497a7a69..bdd7551e56 100644 --- a/src/celeritas/CMakeLists.txt +++ b/src/celeritas/CMakeLists.txt @@ -85,6 +85,7 @@ list(APPEND SOURCES optical/MaterialParams.cc optical/TrackInitParams.cc optical/ScintillationParams.cc + optical/WavelengthShiftParams.cc optical/action/ActionGroups.cc optical/action/LocateVacanciesAction.cc optical/detail/OffloadParams.cc diff --git a/src/celeritas/optical/CerenkovParams.cc b/src/celeritas/optical/CerenkovParams.cc index 7500391528..bbcf71f031 100644 --- a/src/celeritas/optical/CerenkovParams.cc +++ b/src/celeritas/optical/CerenkovParams.cc @@ -49,6 +49,7 @@ CerenkovParams::CerenkovParams(SPConstMaterial material) std::vector integral(energy.size()); for (size_type i = 1; i < energy.size(); ++i) { + // TODO: use trapezoidal integrator helper class integral[i] = integral[i - 1] + real_type(0.5) * (energy[i] - energy[i - 1]) * (1 / ipow<2>(refractive_index[i - 1]) diff --git a/src/celeritas/optical/Interaction.hh b/src/celeritas/optical/Interaction.hh index 6546c21058..78e3421e58 100644 --- a/src/celeritas/optical/Interaction.hh +++ b/src/celeritas/optical/Interaction.hh @@ -8,7 +8,9 @@ #pragma once #include "corecel/Macros.hh" +#include "corecel/cont/Span.hh" #include "geocel/Types.hh" +#include "celeritas/optical/TrackInitializer.hh" namespace celeritas { @@ -29,10 +31,12 @@ struct Interaction scattered, //!< Still alive, state has changed absorbed, //!< Absorbed by the material unchanged, //!< No state change, no secondaries + failed, //!< Ran out of memory during sampling }; Real3 direction; //!< Post-interaction direction Real3 polarization; //!< Post-interaction polarization + Span secondaries; //!< Emitted secondaries Action action{Action::scattered}; //!< Flags for interaction result //! Return an interaction respresenting an absorbed process @@ -41,6 +45,9 @@ struct Interaction //! Return an interaction with no change in the track state static inline CELER_FUNCTION Interaction from_unchanged(); + // Return an interaction representing a recoverable error + static inline CELER_FUNCTION Interaction from_failure(); + //! Whether the state changed but did not fail CELER_FUNCTION bool changed() const { @@ -72,6 +79,17 @@ CELER_FUNCTION Interaction Interaction::from_unchanged() return result; } +//---------------------------------------------------------------------------// +/*! + * Indicate a failure to allocate memory for secondaries. + */ +CELER_FUNCTION Interaction Interaction::from_failure() +{ + Interaction result; + result.action = Action::failed; + return result; +} + //---------------------------------------------------------------------------// } // namespace optical } // namespace celeritas diff --git a/src/celeritas/optical/WavelengthShiftData.hh b/src/celeritas/optical/WavelengthShiftData.hh new file mode 100644 index 0000000000..1fa2fab6d6 --- /dev/null +++ b/src/celeritas/optical/WavelengthShiftData.hh @@ -0,0 +1,80 @@ +//----------------------------------*-C++-*----------------------------------// +// Copyright 2024 UT-Battelle, LLC, and other Celeritas developers. +// See the top-level COPYRIGHT file for details. +// SPDX-License-Identifier: (Apache-2.0 OR MIT) +//---------------------------------------------------------------------------// +//! \file celeritas/optical/WavelengthShiftData.hh +//---------------------------------------------------------------------------// +#pragma once + +#include "corecel/Macros.hh" +#include "corecel/Types.hh" +#include "corecel/data/Collection.hh" +#include "celeritas/Types.hh" +#include "celeritas/grid/GenericGridData.hh" + +namespace celeritas +{ +namespace optical +{ +//---------------------------------------------------------------------------// +/*! + * Material dependent scalar property of wavelength shift (WLS). + */ +struct WlsMaterialRecord +{ + real_type mean_num_photons{}; //!< Mean number of reemitted photons + real_type time_constant{}; //!< Time delay of WLS [time] + + //! Whether all data are assigned and valid + explicit CELER_FUNCTION operator bool() const + { + return mean_num_photons > 0 && time_constant > 0; + } +}; + +//---------------------------------------------------------------------------// +/*! + * Wavelength shift data + */ +template +struct WavelengthShiftData +{ + template + using Items = Collection; + template + using OpticalMaterialItems = Collection; + + //// MEMBER DATA //// + + OpticalMaterialItems wls_record; + + // Grid energy tabulated as a function of the cumulative probability. + OpticalMaterialItems energy_cdf; + + // Backend data + Items reals; + + //// MEMBER FUNCTIONS //// + + //! Whether all data are assigned and valid + explicit CELER_FUNCTION operator bool() const + { + return !wls_record.empty() && !energy_cdf.empty() && !reals.empty(); + } + + //! Assign from another set of data + template + WavelengthShiftData& operator=(WavelengthShiftData const& other) + { + CELER_EXPECT(other); + wls_record = other.wls_record; + energy_cdf = other.energy_cdf; + reals = other.reals; + return *this; + } +}; + +//---------------------------------------------------------------------------// +} // namespace optical +} // namespace celeritas diff --git a/src/celeritas/optical/WavelengthShiftParams.cc b/src/celeritas/optical/WavelengthShiftParams.cc new file mode 100644 index 0000000000..d8a01eb1ce --- /dev/null +++ b/src/celeritas/optical/WavelengthShiftParams.cc @@ -0,0 +1,108 @@ +//----------------------------------*-C++-*----------------------------------// +// Copyright 2024 UT-Battelle, LLC, and other Celeritas developers. +// See the top-level COPYRIGHT file for details. +// SPDX-License-Identifier: (Apache-2.0 OR MIT) +//---------------------------------------------------------------------------// +//! \file celeritas/optical/WavelengthShiftParams.cc +//---------------------------------------------------------------------------// +#include "WavelengthShiftParams.hh" + +#include + +#include "corecel/data/CollectionBuilder.hh" +#include "celeritas/Types.hh" +#include "celeritas/grid/GenericGridBuilder.hh" +#include "celeritas/grid/GenericGridInserter.hh" +#include "celeritas/io/ImportData.hh" + +namespace celeritas +{ +namespace optical +{ +//---------------------------------------------------------------------------// +/*! + * Construct wavelength shift (WLS) data with imported data. + */ +std::shared_ptr +WavelengthShiftParams::from_import(ImportData const& data) +{ + CELER_EXPECT(!data.optical_materials.empty()); + + if (!std::any_of( + data.optical_materials.begin(), + data.optical_materials.end(), + [](auto const& iter) { return static_cast(iter.wls); })) + { + // No wavelength shift data present + return nullptr; + } + + Input input; + for (auto const& mat : data.optical_materials) + { + input.data.push_back(mat.wls); + } + return std::make_shared(std::move(input)); +} + +//---------------------------------------------------------------------------// +/*! + * Construct with wavelength shift (WLS) input data. + */ +WavelengthShiftParams::WavelengthShiftParams(Input const& input) +{ + CELER_EXPECT(input.data.size() > 0); + HostVal data; + + CollectionBuilder wls_record{&data.wls_record}; + GenericGridInserter insert_energy_cdf(&data.reals, &data.energy_cdf); + for (auto const& wls : input.data) + { + if (!wls) + { + // No WLS data for this material + wls_record.push_back({}); + insert_energy_cdf(); + continue; + } + + // WLS material properties + WlsMaterialRecord record; + record.mean_num_photons = wls.mean_num_photons; + record.time_constant = wls.time_constant; + wls_record.push_back(record); + + // Calculate the WLS cumulative probability of the emission spectrum + // Store WLS component tabulated as a function of photon energy + auto const& comp_vec = wls.component; + std::vector cdf(comp_vec.x.size()); + + CELER_ASSERT(comp_vec.y[0] > 0); + // The value of cdf at the low edge is zero by default + cdf[0] = 0; + for (size_type i = 1; i < comp_vec.x.size(); ++i) + { + // TODO: use trapezoidal integrator helper class + cdf[i] = cdf[i - 1] + + 0.5 * (comp_vec.x[i] - comp_vec.x[i - 1]) + * (comp_vec.y[i] + comp_vec.y[i - 1]); + } + + // Normalize for the cdf probability + for (size_type i = 1; i < comp_vec.x.size(); ++i) + { + cdf[i] = cdf[i] / cdf.back(); + } + // Note that energy and cdf are swapped for the inverse sampling + insert_energy_cdf(make_span(cdf), make_span(comp_vec.x)); + } + CELER_ASSERT(data.energy_cdf.size() == input.data.size()); + CELER_ASSERT(data.wls_record.size() == data.energy_cdf.size()); + + data_ = CollectionMirror{std::move(data)}; + CELER_ENSURE(data_); +} + +//---------------------------------------------------------------------------// +} // namespace optical +} // namespace celeritas diff --git a/src/celeritas/optical/WavelengthShiftParams.hh b/src/celeritas/optical/WavelengthShiftParams.hh new file mode 100644 index 0000000000..4ec9ca1eca --- /dev/null +++ b/src/celeritas/optical/WavelengthShiftParams.hh @@ -0,0 +1,58 @@ +//----------------------------------*-C++-*----------------------------------// +// Copyright 2024 UT-Battelle, LLC, and other Celeritas developers. +// See the top-level COPYRIGHT file for details. +// SPDX-License-Identifier: (Apache-2.0 OR MIT) +//---------------------------------------------------------------------------// +//! \file celeritas/optical/WavelengthShiftParams.hh +//---------------------------------------------------------------------------// +#pragma once + +#include "corecel/Types.hh" +#include "corecel/data/CollectionMirror.hh" +#include "corecel/data/ParamsDataInterface.hh" +#include "celeritas/io/ImportOpticalMaterial.hh" + +#include "WavelengthShiftData.hh" + +namespace celeritas +{ +struct ImportData; + +namespace optical +{ +//---------------------------------------------------------------------------// +/*! + * Build and manage wavelength shift (WLS) data. + */ +class WavelengthShiftParams final + : public ParamsDataInterface +{ + public: + //! Material-dependent WLS data, indexed by \c OpticalMaterialId + struct Input + { + std::vector data; + }; + + public: + // Construct with imported data + static std::shared_ptr + from_import(ImportData const& data); + + // Construct with WLS input data + explicit WavelengthShiftParams(Input const& input); + + //! Access WLS data on the host + HostRef const& host_ref() const final { return data_.host_ref(); } + + //! Access WLS data on the device + DeviceRef const& device_ref() const final { return data_.device_ref(); } + + private: + // Host/device storage and reference + CollectionMirror data_; +}; + +//---------------------------------------------------------------------------// +} // namespace optical +} // namespace celeritas diff --git a/src/celeritas/optical/interactor/WavelengthShiftInteractor.hh b/src/celeritas/optical/interactor/WavelengthShiftInteractor.hh new file mode 100644 index 0000000000..da7849d8ed --- /dev/null +++ b/src/celeritas/optical/interactor/WavelengthShiftInteractor.hh @@ -0,0 +1,180 @@ +//----------------------------------*-C++-*----------------------------------// +// Copyright 2024 UT-Battelle, LLC, and other Celeritas developers. +// See the top-level COPYRIGHT file for details. +// SPDX-License-Identifier: (Apache-2.0 OR MIT) +//---------------------------------------------------------------------------// +//! \file celeritas/optical/interactor/WavelengthShiftInteractor.hh +//---------------------------------------------------------------------------// +#pragma once + +#include "corecel/Macros.hh" +#include "corecel/Types.hh" +#include "corecel/data/StackAllocator.hh" +#include "celeritas/Types.hh" +#include "celeritas/grid/GenericCalculator.hh" +#include "celeritas/optical/Interaction.hh" +#include "celeritas/optical/ParticleTrackView.hh" +#include "celeritas/optical/TrackInitializer.hh" +#include "celeritas/optical/WavelengthShiftData.hh" +#include "celeritas/phys/InteractionUtils.hh" +#include "celeritas/random/distribution/ExponentialDistribution.hh" +#include "celeritas/random/distribution/IsotropicDistribution.hh" +#include "celeritas/random/distribution/PoissonDistribution.hh" +#include "celeritas/random/distribution/UniformRealDistribution.hh" + +namespace celeritas +{ +namespace optical +{ +//---------------------------------------------------------------------------// +/*! + * Apply the wavelength shift (WLS) to optical photons. + * + * A wavelength shifter absorbs an incident light and reemits secondary lights + * isotropically at longer wavelengths. It usually shifts the ultraviolet + * region of the radiation spectrum to the visible region, which enhances the + * light collection or reduces the self-absorption of the optical production. + * The number of the reemitted photons follows the Poisson distribution with + * the mean number of the characteristic light production, which depends on the + * optical property of wavelength shifters. The polarization of the reemitted + * lights is assumed to be incoherent with respect to the polarization of the + * primary optical photon. + * + * \note This performs the same sampling routine as in the G4OpWLS class of + * the Geant4 release 11.2. + */ +class WavelengthShiftInteractor +{ + public: + //!@{ + //! \name Type aliases + using Energy = units::MevEnergy; + using ParamsRef = NativeCRef; + using SecondaryAllocator = StackAllocator; + //!@} + + public: + // Construct with shared and state data + inline CELER_FUNCTION + WavelengthShiftInteractor(ParamsRef const& shared, + ParticleTrackView const& particle, + OpticalMaterialId const& mat_id, + SecondaryAllocator& allocate); + + // Sample an interaction with the given RNG + template + inline CELER_FUNCTION Interaction operator()(Engine& rng); + + private: + //// DATA //// + + // Incident photon energy + Energy const inc_energy_; + // Sample distributions + PoissonDistribution sample_num_photons_; + ExponentialDistribution sample_time_; + // Grid calculators + GenericCalculator calc_energy_; + GenericCalculator calc_cdf_; + // Allocate space for secondary particles + SecondaryAllocator& allocate_; +}; + +//---------------------------------------------------------------------------// +// INLINE DEFINITIONS +//---------------------------------------------------------------------------// +/*! + * Construct with shared and state data. + */ +CELER_FUNCTION +WavelengthShiftInteractor::WavelengthShiftInteractor( + ParamsRef const& shared, + ParticleTrackView const& particle, + OpticalMaterialId const& mat_id, + SecondaryAllocator& allocate) + : inc_energy_(particle.energy()) + , sample_num_photons_(shared.wls_record[mat_id].mean_num_photons) + , sample_time_(real_type{1} / shared.wls_record[mat_id].time_constant) + , calc_energy_(shared.energy_cdf[mat_id], shared.reals) + , calc_cdf_(GenericCalculator::from_inverse(shared.energy_cdf[mat_id], + shared.reals)) + , allocate_(allocate) +{ + CELER_EXPECT(inc_energy_.value() > calc_energy_(0)); +} + +//---------------------------------------------------------------------------// +// INLINE DEFINITIONS +//---------------------------------------------------------------------------// +/*! + * Sampling the wavelength shift (WLS) photons. + */ +template +CELER_FUNCTION Interaction WavelengthShiftInteractor::operator()(Engine& rng) +{ + /*! + * Sample the number of photons generated from WLS. + * + * \todo if this is nonzero and allocation fails, we lose reproducibility. + */ + unsigned int num_photons = sample_num_photons_(rng); + + if (num_photons == 0) + { + // Return absorbed photon without reemitted optical photons + return Interaction::from_absorption(); + } + + // Allocate space for reemitted optical photons - Note: the reproducibility + // is not guaranteed in the case of an out-of-memory error + TrackInitializer* secondaries = allocate_(num_photons); + + if (secondaries == nullptr) + { + // Failed to allocate space + return Interaction::from_failure(); + } + + // Sample wavelength shifted optical photons + Interaction result = Interaction::from_absorption(); + result.secondaries = {secondaries, num_photons}; + + IsotropicDistribution sample_direction{}; + for (size_type i : range(num_photons)) + { + // Sample the emitted energy from the inverse cumulative distribution + // TODO: add CDF sampler; see + // https://github.com/celeritas-project/celeritas/pull/1507/files#r1844973621 + real_type energy = calc_energy_(generate_canonical(rng)); + if (CELER_UNLIKELY(energy > inc_energy_.value())) + { + // Sample a restricted energy below the incident photon energy + real_type cdf_max = calc_cdf_(inc_energy_.value()); + UniformRealDistribution sample_cdf(0, cdf_max); + energy = calc_energy_(sample_cdf(rng)); + } + CELER_ENSURE(energy < inc_energy_.value()); + secondaries[i].energy = Energy{energy}; + + // Sample the emitted photon (incoherent) direction and polarization + secondaries[i].direction = sample_direction(rng); + secondaries[i].polarization + = ExitingDirectionSampler{0, secondaries[i].direction}(rng); + + CELER_ENSURE(is_soft_unit_vector(secondaries[i].direction)); + CELER_ENSURE(is_soft_unit_vector(secondaries[i].polarization)); + CELER_ENSURE(soft_zero(dot_product(secondaries[i].direction, + secondaries[i].polarization))); + + // Sample the delta time (based on the exponential relaxation) + secondaries[i].time = sample_time_(rng); + } + + CELER_ENSURE(result.action == Interaction::Action::absorbed); + + return result; +} + +//---------------------------------------------------------------------------// +} // namespace optical +} // namespace celeritas diff --git a/test/celeritas/CMakeLists.txt b/test/celeritas/CMakeLists.txt index 4b917c170a..6c68d51874 100644 --- a/test/celeritas/CMakeLists.txt +++ b/test/celeritas/CMakeLists.txt @@ -311,6 +311,7 @@ celeritas_add_test(optical/OpticalUtils.test.cc) celeritas_add_test(optical/Scintillation.test.cc) celeritas_add_test(optical/Rayleigh.test.cc ${_needs_double}) celeritas_add_test(optical/Absorption.test.cc ${_needs_double}) +celeritas_add_test(optical/WavelengthShift.test.cc ${_needs_double}) celeritas_add_test(optical/ImportedModelAdapter.test.cc) celeritas_add_test(optical/MfpBuilder.test.cc) celeritas_add_test(optical/RayleighMfpCalculator.test.cc) diff --git a/test/celeritas/optical/InteractorHostTestBase.cc b/test/celeritas/optical/InteractorHostTestBase.cc index 4bd97fbf86..7d209cc3f0 100644 --- a/test/celeritas/optical/InteractorHostTestBase.cc +++ b/test/celeritas/optical/InteractorHostTestBase.cc @@ -29,6 +29,9 @@ InteractorHostTestBase::InteractorHostTestBase() : inc_direction_({0, 0, 1}) ps_ = StateStore(1); pt_view_ = std::make_shared(ps_.ref(), TrackSlotId{0}); *pt_view_ = ParticleTrackView::Initializer{Energy{13e-6}, Real3{1, 0, 0}}; + + // Set default capacities + this->resize_secondaries(128); } //---------------------------------------------------------------------------// @@ -123,6 +126,18 @@ void InteractorHostTestBase::check_direction_polarization( interaction.polarization); } +//---------------------------------------------------------------------------// +/*! + * Resize secondaries. + */ +void InteractorHostTestBase::resize_secondaries(int count) +{ + CELER_EXPECT(count > 0); + secondaries_ = StateStore(count); + sa_view_ = std::make_shared>( + secondaries_.ref()); +} + //---------------------------------------------------------------------------// } // namespace test } // namespace optical diff --git a/test/celeritas/optical/InteractorHostTestBase.hh b/test/celeritas/optical/InteractorHostTestBase.hh index 1b31716141..bda3b35895 100644 --- a/test/celeritas/optical/InteractorHostTestBase.hh +++ b/test/celeritas/optical/InteractorHostTestBase.hh @@ -10,7 +10,9 @@ #include #include +#include "corecel/cont/Span.hh" #include "corecel/data/CollectionStateStore.hh" +#include "corecel/data/StackAllocator.hh" #include "celeritas/Quantities.hh" #include "celeritas/optical/Interaction.hh" #include "celeritas/optical/ParticleData.hh" @@ -42,6 +44,8 @@ class InteractorHostTestBase : public Test using RandomEngine = DiagnosticRngEngine; using Energy = units::MevEnergy; using Action = Interaction::Action; + using SecondaryAllocator = StackAllocator; + using constSpanSecondaries = Span; //!@} public: @@ -69,6 +73,14 @@ class InteractorHostTestBase : public Test //! Get incident photon track view ParticleTrackView const& particle_track() const; + //! Secondary stack storage and access + void resize_secondaries(int count); + SecondaryAllocator& secondary_allocator() + { + CELER_EXPECT(sa_view_); + return *sa_view_; + } + //!@{ //! Check direction and polarizations are physical void check_direction_polarization(Real3 const& dir, Real3 const& pol) const; @@ -78,12 +90,16 @@ class InteractorHostTestBase : public Test private: template class S> using StateStore = CollectionStateStore; + template + using SecondaryStackData = StackAllocatorData; StateStore ps_; + StateStore secondaries_; RandomEngine rng_; Real3 inc_direction_; std::shared_ptr pt_view_; + std::shared_ptr sa_view_; }; //---------------------------------------------------------------------------// diff --git a/test/celeritas/optical/WavelengthShift.test.cc b/test/celeritas/optical/WavelengthShift.test.cc new file mode 100644 index 0000000000..116237a660 --- /dev/null +++ b/test/celeritas/optical/WavelengthShift.test.cc @@ -0,0 +1,244 @@ +//----------------------------------*-C++-*----------------------------------// +// Copyright 2024 UT-Battelle, LLC, and other Celeritas developers. +// See the top-level COPYRIGHT file for details. +// SPDX-License-Identifier: (Apache-2.0 OR MIT) +//---------------------------------------------------------------------------// +//! \file celeritas/optical/WavelengthShift.test.cc +//---------------------------------------------------------------------------// +#include + +#include "corecel/cont/Range.hh" +#include "corecel/math/Quantity.hh" +#include "celeritas/UnitTypes.hh" +#include "celeritas/grid/GenericCalculator.hh" +#include "celeritas/io/ImportOpticalMaterial.hh" +#include "celeritas/optical/WavelengthShiftParams.hh" +#include "celeritas/optical/interactor/WavelengthShiftInteractor.hh" + +#include "InteractorHostTestBase.hh" +#include "celeritas_test.hh" + +namespace celeritas +{ +namespace optical +{ +namespace test +{ +using TimeSecond = celeritas::Quantity; + +//---------------------------------------------------------------------------// +/*! + * Tabulated spectrum as a function of the reemitted photon energy [MeV]. + */ +Span get_energy() +{ + static real_type const energy[] = {1.65e-6, 2e-6, 2.4e-6, 2.8e-6, 3.26e-6}; + return make_span(energy); +} + +Span get_spectrum() +{ + static real_type const spectrum[] = {0.15, 0.25, 0.50, 0.40, 0.02}; + return make_span(spectrum); +} + +//---------------------------------------------------------------------------// +// TEST HARNESS +//---------------------------------------------------------------------------// + +class WavelengthShiftTest : public InteractorHostTestBase +{ + protected: + using HostDataCRef = HostCRef; + + void SetUp() override + { + // Build wavelength shift (WLS) property data from a test input + ImportWavelengthShift wls; + wls.mean_num_photons = 2; + wls.time_constant = native_value_from(TimeSecond(1e-9)); + // Reemitted photon energy range (visible light) + wls.component.x = {get_energy().begin(), get_energy().end()}; + // Reemitted photon energy spectrum + wls.component.y = {get_spectrum().begin(), get_spectrum().end()}; + wls.component.vector_type = ImportPhysicsVectorType::free; + + WavelengthShiftParams::Input input; + input.data.push_back(std::move(wls)); + params_ = std::make_shared(input); + data_ = params_->host_ref(); + } + + OpticalMaterialId material_id_{0}; + std::shared_ptr params_; + HostDataCRef data_; +}; + +//---------------------------------------------------------------------------// +// TESTS +//---------------------------------------------------------------------------// + +TEST_F(WavelengthShiftTest, data) +{ + // Test the material properties of WLS + WlsMaterialRecord wls_record = data_.wls_record[material_id_]; + EXPECT_SOFT_EQ(2, wls_record.mean_num_photons); + EXPECT_SOFT_EQ(1 * units::nanosecond, wls_record.time_constant); + + // Test the vector property (emission spectrum) of WLS + auto const& grid = data_.energy_cdf[material_id_]; + EXPECT_TRUE(grid); + + auto const& cdf = data_.reals[grid.grid]; + EXPECT_EQ(5, cdf.size()); + EXPECT_SOFT_EQ(0, cdf.front()); + EXPECT_SOFT_EQ(1, cdf.back()); + + auto const& energy = data_.reals[grid.value]; + EXPECT_EQ(5, energy.size()); + EXPECT_SOFT_EQ(1.65e-6, energy.front()); + EXPECT_SOFT_EQ(3.26e-6, energy.back()); + + // Test the energy range and spectrum of emitted photons + GenericCalculator calc_energy(data_.energy_cdf[material_id_], data_.reals); + + EXPECT_SOFT_EQ(energy.front(), calc_energy(0)); + EXPECT_SOFT_EQ(energy.back(), calc_energy(1)); + + std::vector wls_energy; + + for ([[maybe_unused]] auto i : range(4)) + { + wls_energy.push_back(calc_energy(generate_canonical(this->rng()))); + } + + static double const expected_wls_energy[] = {1.98638940166891e-06, + 2.86983459900623e-06, + 3.18637969114425e-06, + 2.1060413486539e-06}; + + EXPECT_VEC_SOFT_EQ(expected_wls_energy, wls_energy); +} + +TEST_F(WavelengthShiftTest, wls_basic) +{ + int const num_samples = 4; + this->resize_secondaries(num_samples * 2); + auto& rng_engine = this->rng(); + + // Interactor with an energy point within the input component range + real_type test_energy = 2e-6; + this->set_inc_energy(Energy{test_energy}); + + WavelengthShiftInteractor interactor(data_, + this->particle_track(), + material_id_, + this->secondary_allocator()); + + std::vector num_secondaries; + + for ([[maybe_unused]] int i : range(num_samples)) + { + Interaction result = interactor(rng_engine); + size_type num_emitted = result.secondaries.size(); + + num_secondaries.push_back(num_emitted); + + EXPECT_EQ(Interaction::Action::absorbed, result.action); + for (auto j : range(num_emitted)) + { + EXPECT_LT(result.secondaries[j].energy.value(), test_energy); + EXPECT_SOFT_EQ(0, + dot_product(result.secondaries[j].polarization, + result.secondaries[j].direction)); + } + } + static size_type const expected_num_secondaries[] = {1, 4, 3, 0}; + EXPECT_VEC_EQ(expected_num_secondaries, num_secondaries); +} + +TEST_F(WavelengthShiftTest, wls_stress) +{ + int const num_samples = 128; + + WlsMaterialRecord wls_record = data_.wls_record[material_id_]; + this->resize_secondaries( + num_samples * static_cast(wls_record.mean_num_photons) * 4); + auto& rng_engine = this->rng(); + Real3 const inc_dir = {0, 0, 1}; + + std::vector avg_emitted; + std::vector avg_energy; + std::vector avg_costheta; + std::vector avg_orthogonality; + std::vector avg_time; + + // Interactor with points above the reemission spectrum + for (real_type inc_e : {5., 10., 50., 100.}) + { + this->set_inc_energy(Energy{inc_e}); + WavelengthShiftInteractor interactor(data_, + this->particle_track(), + material_id_, + this->secondary_allocator()); + + size_type sum_emitted{}; + real_type sum_energy{}; + real_type sum_costheta{}; + real_type sum_orthogonality{}; + real_type sum_time{}; + + for ([[maybe_unused]] int i : range(num_samples)) + { + Interaction result = interactor(rng_engine); + size_type num_emitted = result.secondaries.size(); + + sum_emitted += num_emitted; + for (auto j : range(num_emitted)) + { + sum_energy += result.secondaries[j].energy.value(); + sum_costheta + += dot_product(result.secondaries[j].direction, inc_dir); + sum_orthogonality + += dot_product(result.secondaries[j].polarization, + result.secondaries[j].direction); + sum_time += result.secondaries[j].time; + } + } + avg_emitted.push_back(static_cast(sum_emitted) / num_samples); + avg_energy.push_back(sum_energy / sum_emitted); + avg_costheta.push_back(sum_costheta / sum_emitted); + avg_orthogonality.push_back(sum_orthogonality / sum_emitted); + avg_time.push_back(sum_time / sum_emitted / units::second); + } + + static double const expected_avg_emitted[] + = {1.96875, 1.890625, 2.0234375, 2.0703125}; + + static double const expected_avg_energy[] = {2.44571770464513e-06, + 2.47500490691662e-06, + 2.4162395900554e-06, + 2.46151256760185e-06}; + + static double const expected_avg_costheta[] = {0.0157611129315312, + 0.0325629374415683, + 0.0082191738981211, + 0.0128202506207409}; + + static double const expected_avg_orthogonality[] = {0, 0, 0, 0}; + + static double const expected_avg_time[] = {1.08250310854364e-09, + 1.01943566892086e-09, + 1.05383398761085e-09, + 9.64465413967612e-10}; + + EXPECT_VEC_EQ(expected_avg_emitted, avg_emitted); + EXPECT_VEC_SOFT_EQ(expected_avg_energy, avg_energy); + EXPECT_VEC_SOFT_EQ(expected_avg_costheta, avg_costheta); + EXPECT_VEC_SOFT_EQ(expected_avg_orthogonality, avg_orthogonality); + EXPECT_VEC_SOFT_EQ(expected_avg_time, avg_time); +} +//---------------------------------------------------------------------------// +} // namespace test +} // namespace optical +} // namespace celeritas From ce82463dfcc0543b440431e942eaaad192f38f96 Mon Sep 17 00:00:00 2001 From: Amanda Lund Date: Wed, 4 Dec 2024 07:06:53 -0600 Subject: [PATCH 06/15] Rename Cerenkov to Cherenkov (#1533) * Rename Cerenkov --> Cherenkov * Update docs --- app/celer-sim/Runner.cc | 6 +- doc/implementation/optical-physics.rst | 18 ++-- src/celeritas/CMakeLists.txt | 6 +- .../ext/GeantOpticalPhysicsOptions.hh | 18 ++-- .../ext/GeantOpticalPhysicsOptionsIO.json.cc | 10 +- .../ext/detail/CelerOpticalPhysics.cc | 42 ++++----- src/celeritas/io/ImportOpticalMaterial.hh | 4 +- .../{CerenkovData.hh => CherenkovData.hh} | 8 +- ...lculator.hh => CherenkovDndxCalculator.hh} | 32 +++---- ...nkovGenerator.hh => CherenkovGenerator.hh} | 28 +++--- ...CerenkovOffload.hh => CherenkovOffload.hh} | 30 +++--- .../{CerenkovParams.cc => CherenkovParams.cc} | 12 +-- .../{CerenkovParams.hh => CherenkovParams.hh} | 12 +-- src/celeritas/optical/OffloadData.hh | 22 ++--- src/celeritas/optical/OpticalCollector.cc | 38 ++++---- src/celeritas/optical/OpticalCollector.hh | 24 ++--- ...rAction.cc => CherenkovGeneratorAction.cc} | 42 ++++----- ...rAction.cu => CherenkovGeneratorAction.cu} | 14 +-- ...rAction.hh => CherenkovGeneratorAction.hh} | 20 ++-- ...cutor.hh => CherenkovGeneratorExecutor.hh} | 26 ++--- ...oadAction.cc => CherenkovOffloadAction.cc} | 40 ++++---- ...oadAction.cu => CherenkovOffloadAction.cu} | 14 +-- ...oadAction.hh => CherenkovOffloadAction.hh} | 18 ++-- ...xecutor.hh => CherenkovOffloadExecutor.hh} | 24 ++--- test/celeritas/CMakeLists.txt | 2 +- test/celeritas/GlobalTestBase.hh | 14 +-- test/celeritas/ImportedDataTestBase.cc | 6 +- test/celeritas/ImportedDataTestBase.hh | 2 +- test/celeritas/OnlyCoreTestBase.hh | 2 +- test/celeritas/data/lar-sphere.geant.json | 2 +- .../{Cerenkov.test.cc => Cherenkov.test.cc} | 38 ++++---- .../optical/OpticalCollector.test.cc | 94 +++++++++---------- test/celeritas/optical/OpticalTestBase.cc | 2 +- 33 files changed, 335 insertions(+), 335 deletions(-) rename src/celeritas/optical/{CerenkovData.hh => CherenkovData.hh} (88%) rename src/celeritas/optical/{CerenkovDndxCalculator.hh => CherenkovDndxCalculator.hh} (83%) rename src/celeritas/optical/{CerenkovGenerator.hh => CherenkovGenerator.hh} (88%) rename src/celeritas/optical/{CerenkovOffload.hh => CherenkovOffload.hh} (80%) rename src/celeritas/optical/{CerenkovParams.cc => CherenkovParams.cc} (89%) rename src/celeritas/optical/{CerenkovParams.hh => CherenkovParams.hh} (81%) rename src/celeritas/optical/detail/{CerenkovGeneratorAction.cc => CherenkovGeneratorAction.cc} (80%) rename src/celeritas/optical/detail/{CerenkovGeneratorAction.cu => CherenkovGeneratorAction.cu} (83%) rename src/celeritas/optical/detail/{CerenkovGeneratorAction.hh => CherenkovGeneratorAction.hh} (84%) rename src/celeritas/optical/detail/{CerenkovGeneratorExecutor.hh => CherenkovGeneratorExecutor.hh} (80%) rename src/celeritas/optical/detail/{CerenkovOffloadAction.cc => CherenkovOffloadAction.cc} (77%) rename src/celeritas/optical/detail/{CerenkovOffloadAction.cu => CherenkovOffloadAction.cu} (80%) rename src/celeritas/optical/detail/{CerenkovOffloadAction.hh => CherenkovOffloadAction.hh} (83%) rename src/celeritas/optical/detail/{CerenkovOffloadExecutor.hh => CherenkovOffloadExecutor.hh} (76%) rename test/celeritas/optical/{Cerenkov.test.cc => Cherenkov.test.cc} (95%) diff --git a/app/celer-sim/Runner.cc b/app/celer-sim/Runner.cc index 5291e5f49c..13b5296598 100644 --- a/app/celer-sim/Runner.cc +++ b/app/celer-sim/Runner.cc @@ -47,7 +47,7 @@ #include "celeritas/io/EventReader.hh" #include "celeritas/io/RootEventReader.hh" #include "celeritas/mat/MaterialParams.hh" -#include "celeritas/optical/CerenkovParams.hh" +#include "celeritas/optical/CherenkovParams.hh" #include "celeritas/optical/MaterialParams.hh" #include "celeritas/optical/OpticalCollector.hh" #include "celeritas/optical/ScintillationParams.hh" @@ -587,7 +587,7 @@ void Runner::build_optical_collector(RunnerInput const& inp, { CELER_EXPECT(core_params_); - using optical::CerenkovParams; + using optical::CherenkovParams; using optical::MaterialParams; using optical::ScintillationParams; @@ -604,7 +604,7 @@ void Runner::build_optical_collector(RunnerInput const& inp, OpticalCollector::Input oc_inp; oc_inp.material = MaterialParams::from_import( imported, *core_params_->geomaterial(), *core_params_->material()); - oc_inp.cerenkov = std::make_shared(oc_inp.material); + oc_inp.cherenkov = std::make_shared(oc_inp.material); oc_inp.scintillation = ScintillationParams::from_import(imported, core_params_->particle()); oc_inp.num_track_slots = ceil_div(inp.optical.num_track_slots, num_streams); diff --git a/doc/implementation/optical-physics.rst b/doc/implementation/optical-physics.rst index fd630fc0be..a983c53cdf 100644 --- a/doc/implementation/optical-physics.rst +++ b/doc/implementation/optical-physics.rst @@ -21,7 +21,7 @@ tracking loop: gun["Gun or external"] geant4-direct["Direct Geant4 offload"] geant4-scint["Geant4 scintillation"] - geant4-ceren["Geant4 cerenkov"] + geant4-ceren["Geant4 cherenkov"] classDef not-impl stroke-width:2px,stroke-dasharray: 5 5 class geant4-direct,geant4-scint,geant4-ceren not-impl @@ -29,21 +29,21 @@ tracking loop: subgraph main-celeritas-loop["Main celeritas loop"] offload-gather scintillation-offload - cerenkov-offload + cherenkov-offload end offload-gather -->|pre-step| scintillation-offload - offload-gather -->|pre-step| cerenkov-offload + offload-gather -->|pre-step| cherenkov-offload subgraph photon-gen["Optical photon generation"] scintillation-gen - cerenkov-gen + cherenkov-gen end scintillation-offload -->|generator dist| scintillation-gen - cerenkov-offload -->|generator dist| cerenkov-gen + cherenkov-offload -->|generator dist| cherenkov-gen geant4-scint -->|generator dist| scintillation-gen - geant4-ceren -->|generator dist| cerenkov-gen + geant4-ceren -->|generator dist| cherenkov-gen photons["Optical tracking loop"] @@ -51,7 +51,7 @@ tracking loop: geant4-direct -->|inits| photons scintillation-gen -->|inits| photons - cerenkov-gen -->|inits| photons + cherenkov-gen -->|inits| photons Optical materials @@ -76,7 +76,7 @@ within-step energy distribution are used to "offload" optical photons by generating *distribution parameters* to be sampled in the stepping loop. .. doxygenclass:: celeritas::OpticalCollector -.. doxygenclass:: celeritas::CerenkovOffload +.. doxygenclass:: celeritas::CherenkovOffload .. doxygenclass:: celeritas::ScintillationOffload .. doxygenstruct:: celeritas::optical::GeneratorDistributionData @@ -88,7 +88,7 @@ sample from the distribution of photons specified by the "generator distribution" to create optical photon *initializers* which are analogous to secondary particles in Geant4. -.. doxygenclass:: celeritas::optical::CerenkovGenerator +.. doxygenclass:: celeritas::optical::CherenkovGenerator .. doxygenclass:: celeritas::optical::ScintillationGenerator Volumetric processes diff --git a/src/celeritas/CMakeLists.txt b/src/celeritas/CMakeLists.txt index bdd7551e56..c7d653f0fb 100644 --- a/src/celeritas/CMakeLists.txt +++ b/src/celeritas/CMakeLists.txt @@ -76,7 +76,7 @@ list(APPEND SOURCES neutron/model/CascadeOptionsIO.json.cc neutron/process/NeutronElasticProcess.cc neutron/process/NeutronInelasticProcess.cc - optical/CerenkovParams.cc + optical/CherenkovParams.cc optical/CoreParams.cc optical/CoreState.cc optical/CoreTrackData.cc @@ -294,8 +294,8 @@ celeritas_polysource(optical/action/BoundaryAction) celeritas_polysource(optical/action/detail/TrackInitAlgorithms) celeritas_polysource(optical/action/InitializeTracksAction) celeritas_polysource(optical/action/PreStepAction) -celeritas_polysource(optical/detail/CerenkovGeneratorAction) -celeritas_polysource(optical/detail/CerenkovOffloadAction) +celeritas_polysource(optical/detail/CherenkovGeneratorAction) +celeritas_polysource(optical/detail/CherenkovOffloadAction) celeritas_polysource(optical/detail/OpticalGenAlgorithms) celeritas_polysource(optical/detail/OffloadGatherAction) celeritas_polysource(optical/detail/ScintGeneratorAction) diff --git a/src/celeritas/ext/GeantOpticalPhysicsOptions.hh b/src/celeritas/ext/GeantOpticalPhysicsOptions.hh index 22153eb072..03f50d0b61 100644 --- a/src/celeritas/ext/GeantOpticalPhysicsOptions.hh +++ b/src/celeritas/ext/GeantOpticalPhysicsOptions.hh @@ -22,12 +22,12 @@ enum class WLSTimeProfileSelection }; //---------------------------------------------------------------------------// -//! Cerenkov process options -struct CerenkovPhysicsOptions +//! Cherenkov process options +struct CherenkovPhysicsOptions { //! Enable the process bool enable{true}; - //! Enable generation of Cerenkov photons + //! Enable generation of Cherenkov photons bool stack_photons{true}; //! Track generated photons before parent bool track_secondaries_first{true}; @@ -44,7 +44,7 @@ struct CerenkovPhysicsOptions // TODO: when we require C++20, use `friend bool operator==(...) = // default;` constexpr bool -operator==(CerenkovPhysicsOptions const& a, CerenkovPhysicsOptions const& b) +operator==(CherenkovPhysicsOptions const& a, CherenkovPhysicsOptions const& b) { // clang-format off return a.enable == b.enable @@ -129,8 +129,8 @@ struct GeantOpticalPhysicsOptions { //!@{ //! \name Optical photon creation physics - //! Cerenkov radiation options - CerenkovPhysicsOptions cerenkov; + //! Cherenkov radiation options + CherenkovPhysicsOptions cherenkov; //! Scintillation options ScintillationPhysicsOptions scintillation; //!@} @@ -159,7 +159,7 @@ struct GeantOpticalPhysicsOptions //! True if any process is activated explicit operator bool() const { - return cerenkov || scintillation + return cherenkov || scintillation || (wavelength_shifting != WLSTimeProfileSelection::none) || (wavelength_shifting2 != WLSTimeProfileSelection::none) || boundary || absorption || rayleigh_scattering @@ -170,7 +170,7 @@ struct GeantOpticalPhysicsOptions static GeantOpticalPhysicsOptions deactivated() { GeantOpticalPhysicsOptions opts; - opts.cerenkov.enable = false; + opts.cherenkov.enable = false; opts.scintillation.enable = false; opts.wavelength_shifting = WLSTimeProfileSelection::none; opts.wavelength_shifting2 = WLSTimeProfileSelection::none; @@ -189,7 +189,7 @@ constexpr bool operator==(GeantOpticalPhysicsOptions const& a, GeantOpticalPhysicsOptions const& b) { // clang-format off - return a.cerenkov == b.cerenkov + return a.cherenkov == b.cherenkov && a.scintillation == b.scintillation && a.wavelength_shifting == b.wavelength_shifting && a.wavelength_shifting2 == b.wavelength_shifting2 diff --git a/src/celeritas/ext/GeantOpticalPhysicsOptionsIO.json.cc b/src/celeritas/ext/GeantOpticalPhysicsOptionsIO.json.cc index aec0b8a915..bd73c45353 100644 --- a/src/celeritas/ext/GeantOpticalPhysicsOptionsIO.json.cc +++ b/src/celeritas/ext/GeantOpticalPhysicsOptionsIO.json.cc @@ -34,12 +34,12 @@ void to_json(nlohmann::json& j, WLSTimeProfileSelection const& value) } //---------------------------------------------------------------------------// -void from_json(nlohmann::json const& j, CerenkovPhysicsOptions& options) +void from_json(nlohmann::json const& j, CherenkovPhysicsOptions& options) { if (j.is_null()) { // Null json means deactivated process - options = CerenkovPhysicsOptions{}; + options = CherenkovPhysicsOptions{}; options.enable = false; return; } @@ -52,7 +52,7 @@ void from_json(nlohmann::json const& j, CerenkovPhysicsOptions& options) #undef GCPO_LOAD_OPTION } -void to_json(nlohmann::json& j, CerenkovPhysicsOptions const& inp) +void to_json(nlohmann::json& j, CherenkovPhysicsOptions const& inp) { if (!inp) { @@ -155,7 +155,7 @@ void from_json(nlohmann::json const& j, GeantOpticalPhysicsOptions& options) #define GOPO_LOAD_OPTION(NAME) CELER_JSON_LOAD_OPTION(j, options, NAME) check_format(j, format_str); - GOPO_LOAD_OPTION(cerenkov); + GOPO_LOAD_OPTION(cherenkov); GOPO_LOAD_OPTION(scintillation); GOPO_LOAD_OPTION(wavelength_shifting); GOPO_LOAD_OPTION(wavelength_shifting2); @@ -181,7 +181,7 @@ void to_json(nlohmann::json& j, GeantOpticalPhysicsOptions const& inp) } j = { - CELER_JSON_PAIR(inp, cerenkov), + CELER_JSON_PAIR(inp, cherenkov), CELER_JSON_PAIR(inp, scintillation), CELER_JSON_PAIR(inp, wavelength_shifting), CELER_JSON_PAIR(inp, wavelength_shifting2), diff --git a/src/celeritas/ext/detail/CelerOpticalPhysics.cc b/src/celeritas/ext/detail/CelerOpticalPhysics.cc index c8554d85cb..6d915ce8cd 100644 --- a/src/celeritas/ext/detail/CelerOpticalPhysics.cc +++ b/src/celeritas/ext/detail/CelerOpticalPhysics.cc @@ -45,7 +45,7 @@ namespace */ enum class OpticalProcessType { - cerenkov, + cherenkov, scintillation, absorption, rayleigh, @@ -89,7 +89,7 @@ optical_process_type_to_geant_index(OpticalProcessType value) { switch (value) { - case OpticalProcessType::cerenkov: + case OpticalProcessType::cherenkov: return kCerenkov; case OpticalProcessType::scintillation: return kScintillation; @@ -135,8 +135,8 @@ bool process_is_active( #else switch (process) { - case OpticalProcessType::cerenkov: - return bool(options.cerenkov); + case OpticalProcessType::cherenkov: + return bool(options.cherenkov); case OpticalProcessType::scintillation: return bool(options.scintillation); case OpticalProcessType::absorption: @@ -175,7 +175,7 @@ CelerOpticalPhysics::CelerOpticalPhysics(Options const& options) params->SetProcessActivation(G4OpticalProcessName(i), flag); }; - activate_process(kCerenkov, bool(options_.cerenkov)); + activate_process(kCerenkov, bool(options_.cherenkov)); activate_process(kScintillation, bool(options_.scintillation)); activate_process(kAbsorption, options_.absorption); activate_process(kRayleigh, options_.rayleigh_scattering); @@ -186,12 +186,12 @@ CelerOpticalPhysics::CelerOpticalPhysics(Options const& options) activate_process( kWLS2, options_.wavelength_shifting2 != WLSTimeProfileSelection::none); - // Cerenkov - params->SetCerenkovStackPhotons(options_.cerenkov.stack_photons); + // Cherenkov + params->SetCerenkovStackPhotons(options_.cherenkov.stack_photons); params->SetCerenkovTrackSecondariesFirst( - options_.cerenkov.track_secondaries_first); - params->SetCerenkovMaxPhotonsPerStep(options_.cerenkov.max_photons); - params->SetCerenkovMaxBetaChange(options_.cerenkov.max_beta_change); + options_.cherenkov.track_secondaries_first); + params->SetCerenkovMaxPhotonsPerStep(options_.cherenkov.max_photons); + params->SetCerenkovMaxBetaChange(options_.cherenkov.max_beta_change); // Scintillation params->SetScintStackPhotons(options_.scintillation.stack_photons); @@ -319,13 +319,13 @@ void CelerOpticalPhysics::ConstructProcess() #endif scint->AddSaturation(G4LossTableManager::Instance()->EmSaturation()); - auto cerenkov = ObservingUniquePtr{std::make_unique()}; + auto cherenkov = ObservingUniquePtr{std::make_unique()}; #if G4VERSION_NUMBER < 1070 - cerenkov->SetStackPhotons(options_.cerenkov.stack_photons); - cerenkov->SetTrackSecondariesFirst( - options_.cerenkov.track_secondaries_first); - cerenkov->SetMaxNumPhotonsPerStep(options_.cerenkov.max_photons); - cerenkov->SetMaxBetaChangePerStep(options_.cerenkov.max_beta_change); + cherenkov->SetStackPhotons(options_.cherenkov.stack_photons); + cherenkov->SetTrackSecondariesFirst( + options_.cherenkov.track_secondaries_first); + cherenkov->SetMaxNumPhotonsPerStep(options_.cherenkov.max_photons); + cherenkov->SetMaxBetaChangePerStep(options_.cherenkov.max_beta_change); #endif auto particle_iterator = GetParticleIterator(); @@ -337,12 +337,12 @@ void CelerOpticalPhysics::ConstructProcess() process_manager = p->GetProcessManager(); CELER_ASSERT(process_manager); - if (cerenkov->IsApplicable(*p) - && process_is_active(OpticalProcessType::cerenkov, options_)) + if (cherenkov->IsApplicable(*p) + && process_is_active(OpticalProcessType::cherenkov, options_)) { - process_manager->AddProcess(cerenkov.release()); - process_manager->SetProcessOrdering(cerenkov, idxPostStep); - CELER_LOG(debug) << "Loaded Optical Cerenkov with G4Cerenkov " + process_manager->AddProcess(cherenkov.release()); + process_manager->SetProcessOrdering(cherenkov, idxPostStep); + CELER_LOG(debug) << "Loaded Optical Cherenkov with G4Cerenkov " "process for particle " << p->GetParticleName(); } diff --git a/src/celeritas/io/ImportOpticalMaterial.hh b/src/celeritas/io/ImportOpticalMaterial.hh index ac0959b62f..a7843cdabb 100644 --- a/src/celeritas/io/ImportOpticalMaterial.hh +++ b/src/celeritas/io/ImportOpticalMaterial.hh @@ -164,8 +164,8 @@ struct ImportWavelengthShift /*! * Store optical material properties. * - * \todo boolean for enabling cerenkov in the material?? DUNE e.g. disables - * cerenkov globally. + * \todo boolean for enabling cherenkov in the material?? DUNE e.g. disables + * cherenkov globally. */ struct ImportOpticalMaterial { diff --git a/src/celeritas/optical/CerenkovData.hh b/src/celeritas/optical/CherenkovData.hh similarity index 88% rename from src/celeritas/optical/CerenkovData.hh rename to src/celeritas/optical/CherenkovData.hh index 92ce1bb3ef..04c5bc533e 100644 --- a/src/celeritas/optical/CerenkovData.hh +++ b/src/celeritas/optical/CherenkovData.hh @@ -3,7 +3,7 @@ // See the top-level COPYRIGHT file for details. // SPDX-License-Identifier: (Apache-2.0 OR MIT) //---------------------------------------------------------------------------// -//! \file celeritas/optical/CerenkovData.hh +//! \file celeritas/optical/CherenkovData.hh //---------------------------------------------------------------------------// #pragma once @@ -20,10 +20,10 @@ namespace optical { //---------------------------------------------------------------------------// /*! - * Cerenkov angle integrals tablulated as a function of photon energy. + * Cherenkov angle integrals tablulated as a function of photon energy. */ template -struct CerenkovData +struct CherenkovData { template using Items = Collection; @@ -47,7 +47,7 @@ struct CerenkovData //! Assign from another set of data template - CerenkovData& operator=(CerenkovData const& other) + CherenkovData& operator=(CherenkovData const& other) { CELER_EXPECT(other); angle_integral = other.angle_integral; diff --git a/src/celeritas/optical/CerenkovDndxCalculator.hh b/src/celeritas/optical/CherenkovDndxCalculator.hh similarity index 83% rename from src/celeritas/optical/CerenkovDndxCalculator.hh rename to src/celeritas/optical/CherenkovDndxCalculator.hh index 9e968a97bd..de1f239da9 100644 --- a/src/celeritas/optical/CerenkovDndxCalculator.hh +++ b/src/celeritas/optical/CherenkovDndxCalculator.hh @@ -3,7 +3,7 @@ // See the top-level COPYRIGHT file for details. // SPDX-License-Identifier: (Apache-2.0 OR MIT) //---------------------------------------------------------------------------// -//! \file celeritas/optical/CerenkovDndxCalculator.hh +//! \file celeritas/optical/CherenkovDndxCalculator.hh //---------------------------------------------------------------------------// #pragma once @@ -16,7 +16,7 @@ #include "celeritas/Quantities.hh" #include "celeritas/grid/GenericCalculator.hh" -#include "CerenkovData.hh" +#include "CherenkovData.hh" #include "MaterialView.hh" namespace celeritas @@ -25,7 +25,7 @@ namespace optical { //---------------------------------------------------------------------------// /*! - * Calculate the mean number of Cerenkov photons produced per unit length. + * Calculate the mean number of Cherenkov photons produced per unit length. * * The average number of photons produced is given by * \f[ @@ -49,23 +49,23 @@ namespace optical \frac{1}{n^2(\epsilon)}\dif\epsilon \right]. * \f] */ -class CerenkovDndxCalculator +class CherenkovDndxCalculator { public: - // Construct from optical materials and Cerenkov angle integrals + // Construct from optical materials and Cherenkov angle integrals inline CELER_FUNCTION - CerenkovDndxCalculator(MaterialView const& material, - NativeCRef const& shared, + CherenkovDndxCalculator(MaterialView const& material, + NativeCRef const& shared, units::ElementaryCharge charge); - // Calculate the mean number of Cerenkov photons produced per unit length + // Calculate the mean number of Cherenkov photons produced per unit length inline CELER_FUNCTION real_type operator()(units::LightSpeed beta); private: // Calculate refractive index [MeV -> unitless] GenericCalculator calc_refractive_index_; - // Calculate the Cerenkov angle integral [MeV -> unitless] + // Calculate the Cherenkov angle integral [MeV -> unitless] GenericCalculator calc_integral_; // Square of particle charge @@ -76,12 +76,12 @@ class CerenkovDndxCalculator // INLINE DEFINITIONS //---------------------------------------------------------------------------// /*! - * Construct from optical materials and Cerenkov angle integrals. + * Construct from optical materials and Cherenkov angle integrals. */ CELER_FUNCTION -CerenkovDndxCalculator::CerenkovDndxCalculator( +CherenkovDndxCalculator::CherenkovDndxCalculator( MaterialView const& material, - NativeCRef const& shared, + NativeCRef const& shared, units::ElementaryCharge charge) : calc_refractive_index_(material.make_refractive_index_calculator()) , calc_integral_{shared.angle_integral[material.material_id()], @@ -93,14 +93,14 @@ CerenkovDndxCalculator::CerenkovDndxCalculator( //---------------------------------------------------------------------------// /*! - * Calculate the mean number of Cerenkov photons produced per unit length. + * Calculate the mean number of Cherenkov photons produced per unit length. * * \todo define a "generic grid extents" class for finding the lower/upper x/y * coordinates on grid data. In the future we could cache these if the memory * lookups result in too much indirection. */ CELER_FUNCTION real_type -CerenkovDndxCalculator::operator()(units::LightSpeed beta) +CherenkovDndxCalculator::operator()(units::LightSpeed beta) { CELER_EXPECT(beta.value() > 0 && beta.value() <= 1); @@ -108,7 +108,7 @@ CerenkovDndxCalculator::operator()(units::LightSpeed beta) real_type energy_max = calc_refractive_index_.grid().back(); if (inv_beta > calc_refractive_index_(energy_max)) { - // Incident particle energy is below the threshold for Cerenkov + // Incident particle energy is below the threshold for Cherenkov // emission (i.e., beta < 1 / n_max) return 0; } @@ -134,7 +134,7 @@ CerenkovDndxCalculator::operator()(units::LightSpeed beta) } // Calculate number of photons. This may be negative if the incident - // particle energy is very close to (just above) the Cerenkov production + // particle energy is very close to (just above) the Cherenkov production // threshold return clamp_to_nonneg(zsq_ * (constants::alpha_fine_structure diff --git a/src/celeritas/optical/CerenkovGenerator.hh b/src/celeritas/optical/CherenkovGenerator.hh similarity index 88% rename from src/celeritas/optical/CerenkovGenerator.hh rename to src/celeritas/optical/CherenkovGenerator.hh index 97314741d6..257f185600 100644 --- a/src/celeritas/optical/CerenkovGenerator.hh +++ b/src/celeritas/optical/CherenkovGenerator.hh @@ -3,7 +3,7 @@ // See the top-level COPYRIGHT file for details. // SPDX-License-Identifier: (Apache-2.0 OR MIT) //---------------------------------------------------------------------------// -//! \file celeritas/optical/CerenkovGenerator.hh +//! \file celeritas/optical/CherenkovGenerator.hh //---------------------------------------------------------------------------// #pragma once @@ -20,7 +20,7 @@ #include "celeritas/random/distribution/RejectionSampler.hh" #include "celeritas/random/distribution/UniformRealDistribution.hh" -#include "CerenkovDndxCalculator.hh" +#include "CherenkovDndxCalculator.hh" #include "GeneratorDistributionData.hh" #include "MaterialView.hh" #include "TrackInitializer.hh" @@ -31,9 +31,9 @@ namespace optical { //---------------------------------------------------------------------------// /*! - * Sample Cerenkov photons from the given distribution. + * Sample Cherenkov photons from the given distribution. * - * Cerenkov radiation is emitted when a charged particle passes through a + * Cherenkov radiation is emitted when a charged particle passes through a * dielectric medium faster than the speed of light in that medium. Photons are * emitted on the surface of a cone, with the cone angle, \f$ \theta \f$, * measured with respect to the incident particle direction. As the particle @@ -47,16 +47,16 @@ namespace optical f(\epsilon) = \left[1 - \frac{1}{n^2(\epsilon)\beta^2}\right] * \f] */ -class CerenkovGenerator +class CherenkovGenerator { public: // Construct from optical materials and distribution parameters inline CELER_FUNCTION - CerenkovGenerator(MaterialView const& material, - NativeCRef const& shared, + CherenkovGenerator(MaterialView const& material, + NativeCRef const& shared, GeneratorDistributionData const& dist); - // Sample a Cerenkov photon from the distribution + // Sample a Cherenkov photon from the distribution template inline CELER_FUNCTION TrackInitializer operator()(Generator& rng); @@ -88,8 +88,8 @@ class CerenkovGenerator * Construct from optical materials and distribution parameters. */ CELER_FUNCTION -CerenkovGenerator::CerenkovGenerator(MaterialView const& material, - NativeCRef const& shared, +CherenkovGenerator::CherenkovGenerator(MaterialView const& material, + NativeCRef const& shared, GeneratorDistributionData const& dist) : dist_(dist) , calc_refractive_index_(material.make_refractive_index_calculator()) @@ -105,7 +105,7 @@ CerenkovGenerator::CerenkovGenerator(MaterialView const& material, // pre- and post-step energies auto const& pre_step = dist_.points[StepPoint::pre]; auto const& post_step = dist_.points[StepPoint::post]; - CerenkovDndxCalculator calc_dndx(material, shared, dist_.charge); + CherenkovDndxCalculator calc_dndx(material, shared, dist_.charge); dndx_pre_ = calc_dndx(pre_step.speed); real_type dndx_post = calc_dndx(post_step.speed); @@ -134,10 +134,10 @@ CerenkovGenerator::CerenkovGenerator(MaterialView const& material, //---------------------------------------------------------------------------// /*! - * Sample Cerenkov photons from the distribution. + * Sample Cherenkov photons from the distribution. */ template -CELER_FUNCTION TrackInitializer CerenkovGenerator::operator()(Generator& rng) +CELER_FUNCTION TrackInitializer CherenkovGenerator::operator()(Generator& rng) { // Sample energy and direction real_type energy; @@ -151,7 +151,7 @@ CELER_FUNCTION TrackInitializer CerenkovGenerator::operator()(Generator& rng) // wavelength. // We could improve sampling efficiency for this edge case by // increasing the minimum energy (as is done in - // CerenkovDndxCalculator) to where the refractive index satisfies + // CherenkovDndxCalculator) to where the refractive index satisfies // this condition, but since fewer photons are emitted at lower // energies in general, relatively few rejections will take place // here. diff --git a/src/celeritas/optical/CerenkovOffload.hh b/src/celeritas/optical/CherenkovOffload.hh similarity index 80% rename from src/celeritas/optical/CerenkovOffload.hh rename to src/celeritas/optical/CherenkovOffload.hh index d8a43f9410..fb81e555c8 100644 --- a/src/celeritas/optical/CerenkovOffload.hh +++ b/src/celeritas/optical/CherenkovOffload.hh @@ -3,7 +3,7 @@ // See the top-level COPYRIGHT file for details. // SPDX-License-Identifier: (Apache-2.0 OR MIT) //---------------------------------------------------------------------------// -//! \file celeritas/optical/CerenkovOffload.hh +//! \file celeritas/optical/CherenkovOffload.hh //---------------------------------------------------------------------------// #pragma once @@ -13,8 +13,8 @@ #include "celeritas/random/distribution/PoissonDistribution.hh" #include "celeritas/track/SimTrackView.hh" -#include "CerenkovData.hh" -#include "CerenkovDndxCalculator.hh" +#include "CherenkovData.hh" +#include "CherenkovDndxCalculator.hh" #include "GeneratorDistributionData.hh" #include "MaterialView.hh" #include "OffloadData.hh" @@ -23,10 +23,10 @@ namespace celeritas { //---------------------------------------------------------------------------// /*! - * Sample the number of Cerenkov photons to be generated. + * Sample the number of Cherenkov photons to be generated. * * This populates the \c GeneratorDistributionData used by the \c - * CerenkovGenerator to generate optical photons using post-step and cached + * CherenkovGenerator to generate optical photons using post-step and cached * pre-step data. * * The number of photons is sampled from a Poisson distribution with a mean @@ -35,19 +35,19 @@ namespace celeritas * \f] * where \f$ \ell_\text{step} \f$ is the step length. */ -class CerenkovOffload +class CherenkovOffload { public: - // Construct with optical material, Cerenkov, and step data + // Construct with optical material, Cherenkov, and step data inline CELER_FUNCTION - CerenkovOffload(ParticleTrackView const& particle, + CherenkovOffload(ParticleTrackView const& particle, SimTrackView const& sim, optical::MaterialView const& mat, Real3 const& pos, - NativeCRef const& shared, + NativeCRef const& shared, OffloadPreStepData const& step_data); - // Return a populated optical distribution data for the Cerenkov Generator + // Return a populated optical distribution data for the Cherenkov Generator template inline CELER_FUNCTION optical::GeneratorDistributionData operator()(Generator& rng); @@ -64,14 +64,14 @@ class CerenkovOffload // INLINE DEFINITIONS //---------------------------------------------------------------------------// /*! - * Construct with optical material, Cerenkov, and step information. + * Construct with optical material, Cherenkov, and step information. */ CELER_FUNCTION -CerenkovOffload::CerenkovOffload(ParticleTrackView const& particle, +CherenkovOffload::CherenkovOffload(ParticleTrackView const& particle, SimTrackView const& sim, optical::MaterialView const& mat, Real3 const& pos, - NativeCRef const& shared, + NativeCRef const& shared, OffloadPreStepData const& step_data) : charge_(particle.charge()) , step_length_(sim.step_length()) @@ -85,7 +85,7 @@ CerenkovOffload::CerenkovOffload(ParticleTrackView const& particle, units::LightSpeed beta( real_type{0.5} * (pre_step_.speed.value() + post_step_.speed.value())); - optical::CerenkovDndxCalculator calculate_dndx(mat, shared, charge_); + optical::CherenkovDndxCalculator calculate_dndx(mat, shared, charge_); num_photons_per_len_ = calculate_dndx(beta); } @@ -98,7 +98,7 @@ CerenkovOffload::CerenkovOffload(ParticleTrackView const& particle, */ template CELER_FUNCTION optical::GeneratorDistributionData -CerenkovOffload::operator()(Generator& rng) +CherenkovOffload::operator()(Generator& rng) { if (num_photons_per_len_ == 0) { diff --git a/src/celeritas/optical/CerenkovParams.cc b/src/celeritas/optical/CherenkovParams.cc similarity index 89% rename from src/celeritas/optical/CerenkovParams.cc rename to src/celeritas/optical/CherenkovParams.cc index bbcf71f031..39f2b655f3 100644 --- a/src/celeritas/optical/CerenkovParams.cc +++ b/src/celeritas/optical/CherenkovParams.cc @@ -3,9 +3,9 @@ // See the top-level COPYRIGHT file for details. // SPDX-License-Identifier: (Apache-2.0 OR MIT) //---------------------------------------------------------------------------// -//! \file celeritas/optical/CerenkovParams.cc +//! \file celeritas/optical/CherenkovParams.cc //---------------------------------------------------------------------------// -#include "CerenkovParams.hh" +#include "CherenkovParams.hh" #include #include @@ -28,12 +28,12 @@ namespace optical /*! * Construct with optical property data. */ -CerenkovParams::CerenkovParams(SPConstMaterial material) +CherenkovParams::CherenkovParams(SPConstMaterial material) { CELER_EXPECT(material); auto const& host_ref = material->host_ref(); - HostVal data; + HostVal data; GenericGridInserter insert_angle_integral(&data.reals, &data.angle_integral); @@ -43,7 +43,7 @@ CerenkovParams::CerenkovParams(SPConstMaterial material) auto const& ri_grid = host_ref.refractive_index[mat_id]; CELER_ASSERT(ri_grid); - // Calculate the Cerenkov angle integral + // Calculate the Cherenkov angle integral auto const&& refractive_index = host_ref.reals[ri_grid.value]; auto const&& energy = host_ref.reals[ri_grid.grid]; std::vector integral(energy.size()); @@ -61,7 +61,7 @@ CerenkovParams::CerenkovParams(SPConstMaterial material) CELER_ASSERT(data.angle_integral.size() == host_ref.refractive_index.size()); - data_ = CollectionMirror{std::move(data)}; + data_ = CollectionMirror{std::move(data)}; CELER_ENSURE(data_ || host_ref.refractive_index.empty()); } diff --git a/src/celeritas/optical/CerenkovParams.hh b/src/celeritas/optical/CherenkovParams.hh similarity index 81% rename from src/celeritas/optical/CerenkovParams.hh rename to src/celeritas/optical/CherenkovParams.hh index a0a25900c2..b73ee5c2d9 100644 --- a/src/celeritas/optical/CerenkovParams.hh +++ b/src/celeritas/optical/CherenkovParams.hh @@ -3,7 +3,7 @@ // See the top-level COPYRIGHT file for details. // SPDX-License-Identifier: (Apache-2.0 OR MIT) //---------------------------------------------------------------------------// -//! \file celeritas/optical/CerenkovParams.hh +//! \file celeritas/optical/CherenkovParams.hh //---------------------------------------------------------------------------// #pragma once @@ -11,7 +11,7 @@ #include "corecel/data/CollectionMirror.hh" #include "corecel/data/ParamsDataInterface.hh" -#include "CerenkovData.hh" +#include "CherenkovData.hh" namespace celeritas { @@ -21,9 +21,9 @@ class MaterialParams; //---------------------------------------------------------------------------// /*! - * Build and manage Cerenkov data. + * Build and manage Cherenkov data. */ -class CerenkovParams final : public ParamsDataInterface +class CherenkovParams final : public ParamsDataInterface { public: //!@{ @@ -33,7 +33,7 @@ class CerenkovParams final : public ParamsDataInterface public: // Construct with optical property data - explicit CerenkovParams(SPConstMaterial material); + explicit CherenkovParams(SPConstMaterial material); //! Access physics material on the host HostRef const& host_ref() const final { return data_.host_ref(); } @@ -42,7 +42,7 @@ class CerenkovParams final : public ParamsDataInterface DeviceRef const& device_ref() const final { return data_.device_ref(); } private: - CollectionMirror data_; + CollectionMirror data_; }; //---------------------------------------------------------------------------// diff --git a/src/celeritas/optical/OffloadData.hh b/src/celeritas/optical/OffloadData.hh index 5d806ea872..e97b4a1fbf 100644 --- a/src/celeritas/optical/OffloadData.hh +++ b/src/celeritas/optical/OffloadData.hh @@ -14,7 +14,7 @@ #include "celeritas/Quantities.hh" #include "celeritas/Types.hh" -#include "CerenkovData.hh" +#include "CherenkovData.hh" #include "GeneratorDistributionData.hh" #include "ScintillationData.hh" @@ -28,7 +28,7 @@ namespace celeritas */ struct OffloadBufferSize { - size_type cerenkov{0}; + size_type cherenkov{0}; size_type scintillation{0}; size_type num_photons{0}; }; @@ -37,18 +37,18 @@ struct OffloadBufferSize /*! * Setup options for optical generation. * - * At least one of cerenkov and scintillation must be enabled. + * At least one of cherenkov and scintillation must be enabled. */ struct OffloadOptions { - bool cerenkov{false}; //!< Whether Cerenkov is enabled + bool cherenkov{false}; //!< Whether Cherenkov is enabled bool scintillation{false}; //!< Whether scintillation is enabled size_type capacity{0}; //!< Distribution data buffer capacity //! True if valid explicit CELER_FUNCTION operator bool() const { - return (cerenkov || scintillation) && capacity > 0; + return (cherenkov || scintillation) && capacity > 0; } }; @@ -105,7 +105,7 @@ struct OffloadPreStepData /*! * Optical photon distribution data. * - * The distributions are stored in separate Cerenkov and scintillation buffers + * The distributions are stored in separate Cherenkov and scintillation buffers * indexed by the current buffer size plus the track slot ID. The data is * compacted at the end of each step by removing all invalid distributions. The * order of the distributions in the buffers is guaranteed to be reproducible. @@ -126,7 +126,7 @@ struct OffloadStateData StateItems step; // Buffers of distribution data for generating optical photons - Items cerenkov; + Items cherenkov; Items scintillation; // Determines which distribution a thread will generate a primary from @@ -141,7 +141,7 @@ struct OffloadStateData explicit CELER_FUNCTION operator bool() const { return !step.empty() && !offsets.empty() - && !(cerenkov.empty() && scintillation.empty()); + && !(cherenkov.empty() && scintillation.empty()); } //! Assign from another set of data @@ -150,7 +150,7 @@ struct OffloadStateData { CELER_EXPECT(other); step = other.step; - cerenkov = other.cerenkov; + cherenkov = other.cherenkov; scintillation = other.scintillation; offsets = other.offsets; return *this; @@ -172,9 +172,9 @@ void resize(OffloadStateData* state, resize(&state->step, size); OffloadOptions const& setup = params.setup; - if (setup.cerenkov) + if (setup.cherenkov) { - resize(&state->cerenkov, setup.capacity); + resize(&state->cherenkov, setup.capacity); } if (setup.scintillation) { diff --git a/src/celeritas/optical/OpticalCollector.cc b/src/celeritas/optical/OpticalCollector.cc index 81f0ed930f..07d01c9eba 100644 --- a/src/celeritas/optical/OpticalCollector.cc +++ b/src/celeritas/optical/OpticalCollector.cc @@ -12,14 +12,14 @@ #include "celeritas/global/CoreParams.hh" #include "celeritas/track/TrackInitParams.hh" -#include "CerenkovParams.hh" +#include "CherenkovParams.hh" #include "CoreParams.hh" #include "MaterialParams.hh" #include "OffloadData.hh" #include "ScintillationParams.hh" -#include "detail/CerenkovGeneratorAction.hh" -#include "detail/CerenkovOffloadAction.hh" +#include "detail/CherenkovGeneratorAction.hh" +#include "detail/CherenkovOffloadAction.hh" #include "detail/OffloadGatherAction.hh" #include "detail/OffloadParams.hh" #include "detail/OpticalLaunchAction.hh" @@ -39,7 +39,7 @@ OpticalCollector::OpticalCollector(CoreParams const& core, Input&& inp) CELER_EXPECT(inp); OffloadOptions setup; - setup.cerenkov = inp.cerenkov && inp.material; + setup.cherenkov = inp.cherenkov && inp.material; setup.scintillation = static_cast(inp.scintillation); setup.capacity = inp.buffer_capacity; @@ -55,15 +55,15 @@ OpticalCollector::OpticalCollector(CoreParams const& core, Input&& inp) actions.next_id(), offload_params_->aux_id()); actions.insert(gather_action_); - if (setup.cerenkov) + if (setup.cherenkov) { - // Action to generate Cerenkov optical distributions - cerenkov_action_ = std::make_shared( + // Action to generate Cherenkov optical distributions + cherenkov_action_ = std::make_shared( actions.next_id(), offload_params_->aux_id(), inp.material, - inp.cerenkov); - actions.insert(cerenkov_action_); + inp.cherenkov); + actions.insert(cherenkov_action_); } if (setup.scintillation) @@ -74,20 +74,20 @@ OpticalCollector::OpticalCollector(CoreParams const& core, Input&& inp) actions.insert(scint_action_); } - if (setup.cerenkov) + if (setup.cherenkov) { - // Action to generate Cerenkov primaries - cerenkov_gen_action_ - = std::make_shared( + // Action to generate Cherenkov primaries + cherenkov_gen_action_ + = std::make_shared( actions.next_id(), offload_params_->aux_id(), // TODO: Hack: generator action must be before launch action // but needs optical state aux ID core.aux_reg()->next_id(), inp.material, - std::move(inp.cerenkov), + std::move(inp.cherenkov), inp.auto_flush); - actions.insert(cerenkov_gen_action_); + actions.insert(cherenkov_gen_action_); } if (setup.scintillation) @@ -114,14 +114,14 @@ OpticalCollector::OpticalCollector(CoreParams const& core, Input&& inp) core, std::move(la_inp)); // Launch action must be *after* offload and generator actions - CELER_ENSURE(!cerenkov_action_ + CELER_ENSURE(!cherenkov_action_ || launch_action_->action_id() - > cerenkov_action_->action_id()); + > cherenkov_action_->action_id()); CELER_ENSURE(!scint_action_ || launch_action_->action_id() > scint_action_->action_id()); - CELER_ENSURE(!cerenkov_gen_action_ + CELER_ENSURE(!cherenkov_gen_action_ || launch_action_->action_id() - > cerenkov_gen_action_->action_id()); + > cherenkov_gen_action_->action_id()); CELER_ENSURE(!scint_gen_action_ || launch_action_->action_id() > scint_gen_action_->action_id()); diff --git a/src/celeritas/optical/OpticalCollector.hh b/src/celeritas/optical/OpticalCollector.hh index 431a7ae2a7..03aa36272d 100644 --- a/src/celeritas/optical/OpticalCollector.hh +++ b/src/celeritas/optical/OpticalCollector.hh @@ -22,15 +22,15 @@ class CoreParams; namespace optical { -class CerenkovParams; +class CherenkovParams; class MaterialParams; class ScintillationParams; } // namespace optical namespace detail { -class CerenkovOffloadAction; -class CerenkovGeneratorAction; +class CherenkovOffloadAction; +class CherenkovGeneratorAction; class OffloadGatherAction; class OpticalLaunchAction; class OffloadParams; @@ -45,7 +45,7 @@ class ScintGeneratorAction; * This class is the interface between the main stepping loop and the photon * stepping loop and constructs kernel actions for: * - gathering the pre-step data needed to generate the optical distributions, - * - generating the scintillation and Cerenkov optical distributions at the + * - generating the scintillation and Cherenkov optical distributions at the * end of the step, and * - launching the photon stepping loop. * @@ -62,7 +62,7 @@ class OpticalCollector public: //!@{ //! \name Type aliases - using SPConstCerenkov = std::shared_ptr; + using SPConstCherenkov = std::shared_ptr; using SPConstMaterial = std::shared_ptr; using SPConstScintillation = std::shared_ptr; @@ -72,7 +72,7 @@ class OpticalCollector { //! Optical physics material for materials SPConstMaterial material; - SPConstCerenkov cerenkov; + SPConstCherenkov cherenkov; SPConstScintillation scintillation; //! Number track slots in the optical loop @@ -90,7 +90,7 @@ class OpticalCollector //! True if all input is assigned and valid explicit operator bool() const { - return material && (scintillation || cerenkov) + return material && (scintillation || cherenkov) && num_track_slots > 0 && buffer_capacity > 0 && initializer_capacity > 0 && auto_flush > 0; } @@ -110,11 +110,11 @@ class OpticalCollector //// TYPES //// using SPOffloadParams = std::shared_ptr; - using SPCerenkovAction = std::shared_ptr; + using SPCherenkovAction = std::shared_ptr; using SPScintAction = std::shared_ptr; using SPGatherAction = std::shared_ptr; - using SPCerenkovGenAction - = std::shared_ptr; + using SPCherenkovGenAction + = std::shared_ptr; using SPScintGenAction = std::shared_ptr; using SPLaunchAction = std::shared_ptr; @@ -123,9 +123,9 @@ class OpticalCollector SPOffloadParams offload_params_; SPGatherAction gather_action_; - SPCerenkovAction cerenkov_action_; + SPCherenkovAction cherenkov_action_; SPScintAction scint_action_; - SPCerenkovGenAction cerenkov_gen_action_; + SPCherenkovGenAction cherenkov_gen_action_; SPScintGenAction scint_gen_action_; SPLaunchAction launch_action_; diff --git a/src/celeritas/optical/detail/CerenkovGeneratorAction.cc b/src/celeritas/optical/detail/CherenkovGeneratorAction.cc similarity index 80% rename from src/celeritas/optical/detail/CerenkovGeneratorAction.cc rename to src/celeritas/optical/detail/CherenkovGeneratorAction.cc index f2bbd5ffad..8a2358ae5c 100644 --- a/src/celeritas/optical/detail/CerenkovGeneratorAction.cc +++ b/src/celeritas/optical/detail/CherenkovGeneratorAction.cc @@ -3,9 +3,9 @@ // See the top-level COPYRIGHT file for details. // SPDX-License-Identifier: (Apache-2.0 OR MIT) //---------------------------------------------------------------------------// -//! \file celeritas/optical/detail/CerenkovGeneratorAction.cc +//! \file celeritas/optical/detail/CherenkovGeneratorAction.cc //---------------------------------------------------------------------------// -#include "CerenkovGeneratorAction.hh" +#include "CherenkovGeneratorAction.hh" #include @@ -16,13 +16,13 @@ #include "celeritas/global/CoreParams.hh" #include "celeritas/global/CoreState.hh" #include "celeritas/global/TrackExecutor.hh" -#include "celeritas/optical/CerenkovParams.hh" +#include "celeritas/optical/CherenkovParams.hh" #include "celeritas/optical/CoreParams.hh" #include "celeritas/optical/CoreState.hh" #include "celeritas/optical/CoreTrackData.hh" #include "celeritas/optical/MaterialParams.hh" -#include "CerenkovGeneratorExecutor.hh" +#include "CherenkovGeneratorExecutor.hh" #include "OffloadParams.hh" #include "OpticalGenAlgorithms.hh" @@ -34,23 +34,23 @@ namespace detail /*! * Construct with action ID, data IDs, and optical properties. */ -CerenkovGeneratorAction::CerenkovGeneratorAction(ActionId id, +CherenkovGeneratorAction::CherenkovGeneratorAction(ActionId id, AuxId offload_id, AuxId optical_id, SPConstMaterial material, - SPConstCerenkov cerenkov, + SPConstCherenkov cherenkov, size_type auto_flush) : id_(id) , offload_id_{offload_id} , optical_id_{optical_id} , material_(std::move(material)) - , cerenkov_(std::move(cerenkov)) + , cherenkov_(std::move(cherenkov)) , auto_flush_(auto_flush) { CELER_EXPECT(id_); CELER_EXPECT(offload_id_); CELER_EXPECT(optical_id_); - CELER_EXPECT(cerenkov_); + CELER_EXPECT(cherenkov_); CELER_EXPECT(material_); CELER_EXPECT(auto_flush_ > 0); } @@ -59,16 +59,16 @@ CerenkovGeneratorAction::CerenkovGeneratorAction(ActionId id, /*! * Descriptive name of the action. */ -std::string_view CerenkovGeneratorAction::description() const +std::string_view CherenkovGeneratorAction::description() const { - return "generate Cerenkov photons from optical distribution data"; + return "generate Cherenkov photons from optical distribution data"; } //---------------------------------------------------------------------------// /*! * Execute the action with host data. */ -void CerenkovGeneratorAction::step(CoreParams const& params, +void CherenkovGeneratorAction::step(CoreParams const& params, CoreStateHost& state) const { this->step_impl(params, state); @@ -78,7 +78,7 @@ void CerenkovGeneratorAction::step(CoreParams const& params, /*! * Execute the action with device data. */ -void CerenkovGeneratorAction::step(CoreParams const& params, +void CherenkovGeneratorAction::step(CoreParams const& params, CoreStateDevice& state) const { this->step_impl(params, state); @@ -86,10 +86,10 @@ void CerenkovGeneratorAction::step(CoreParams const& params, //---------------------------------------------------------------------------// /*! - * Generate optical track initializers from Cerenkov distribution data. + * Generate optical track initializers from Cherenkov distribution data. */ template -void CerenkovGeneratorAction::step_impl(CoreParams const& core_params, +void CherenkovGeneratorAction::step_impl(CoreParams const& core_params, CoreState& core_state) const { auto& offload_state @@ -112,20 +112,20 @@ void CerenkovGeneratorAction::step_impl(CoreParams const& core_params, << num_photons << ")"); auto& offload = offload_state.store.ref(); - auto& buffer_size = offload_state.buffer_size.cerenkov; + auto& buffer_size = offload_state.buffer_size.cherenkov; CELER_ASSERT(buffer_size > 0); // Calculate the cumulative sum of the number of photons in the buffered // distributions. These values are used to determine which thread will // generate initializers from which distribution auto count = inclusive_scan_photons( - offload.cerenkov, offload.offsets, buffer_size, core_state.stream_id()); + offload.cherenkov, offload.offsets, buffer_size, core_state.stream_id()); // Generate the optical photon initializers from the distribution data this->generate(core_params, core_state); CELER_LOG_LOCAL(debug) << "Generated " << count - << " Cerenkov photons from " << buffer_size + << " Cherenkov photons from " << buffer_size << " distributions"; num_photons += count; @@ -137,7 +137,7 @@ void CerenkovGeneratorAction::step_impl(CoreParams const& core_params, /*! * Launch a (host) kernel to generate optical photon initializers. */ -void CerenkovGeneratorAction::generate(CoreParams const& core_params, +void CherenkovGeneratorAction::generate(CoreParams const& core_params, CoreStateHost& core_state) const { auto& offload_state = get>( @@ -148,9 +148,9 @@ void CerenkovGeneratorAction::generate(CoreParams const& core_params, TrackExecutor execute{ core_params.ptr(), core_state.ptr(), - detail::CerenkovGeneratorExecutor{core_state.ptr(), + detail::CherenkovGeneratorExecutor{core_state.ptr(), material_->host_ref(), - cerenkov_->host_ref(), + cherenkov_->host_ref(), offload_state.store.ref(), optical_state.ptr(), offload_state.buffer_size, @@ -160,7 +160,7 @@ void CerenkovGeneratorAction::generate(CoreParams const& core_params, //---------------------------------------------------------------------------// #if !CELER_USE_DEVICE -void CerenkovGeneratorAction::generate(CoreParams const&, CoreStateDevice&) const +void CherenkovGeneratorAction::generate(CoreParams const&, CoreStateDevice&) const { CELER_NOT_CONFIGURED("CUDA OR HIP"); } diff --git a/src/celeritas/optical/detail/CerenkovGeneratorAction.cu b/src/celeritas/optical/detail/CherenkovGeneratorAction.cu similarity index 83% rename from src/celeritas/optical/detail/CerenkovGeneratorAction.cu rename to src/celeritas/optical/detail/CherenkovGeneratorAction.cu index bf85a0037d..22cfcc6f3e 100644 --- a/src/celeritas/optical/detail/CerenkovGeneratorAction.cu +++ b/src/celeritas/optical/detail/CherenkovGeneratorAction.cu @@ -3,20 +3,20 @@ // See the top-level COPYRIGHT file for details. // SPDX-License-Identifier: (Apache-2.0 OR MIT) //---------------------------------------------------------------------------// -//! \file celeritas/optical/detail/CerenkovGeneratorAction.cu +//! \file celeritas/optical/detail/CherenkovGeneratorAction.cu //---------------------------------------------------------------------------// -#include "CerenkovGeneratorAction.hh" +#include "CherenkovGeneratorAction.hh" #include "corecel/Assert.hh" #include "corecel/sys/ScopedProfiling.hh" #include "celeritas/global/ActionLauncher.device.hh" #include "celeritas/global/TrackExecutor.hh" -#include "celeritas/optical/CerenkovParams.hh" +#include "celeritas/optical/CherenkovParams.hh" #include "celeritas/optical/CoreParams.hh" #include "celeritas/optical/CoreState.hh" #include "celeritas/optical/MaterialParams.hh" -#include "CerenkovGeneratorExecutor.hh" +#include "CherenkovGeneratorExecutor.hh" #include "OffloadParams.hh" #include "OpticalGenAlgorithms.hh" @@ -28,7 +28,7 @@ namespace detail /*! * Launch a kernel to generate optical photon initializers. */ -void CerenkovGeneratorAction::generate(CoreParams const& core_params, +void CherenkovGeneratorAction::generate(CoreParams const& core_params, CoreStateDevice& core_state) const { auto& offload_state = get>( @@ -39,9 +39,9 @@ void CerenkovGeneratorAction::generate(CoreParams const& core_params, TrackExecutor execute{ core_params.ptr(), core_state.ptr(), - detail::CerenkovGeneratorExecutor{core_state.ptr(), + detail::CherenkovGeneratorExecutor{core_state.ptr(), material_->device_ref(), - cerenkov_->device_ref(), + cherenkov_->device_ref(), offload_state.store.ref(), optical_state.ptr(), offload_state.buffer_size, diff --git a/src/celeritas/optical/detail/CerenkovGeneratorAction.hh b/src/celeritas/optical/detail/CherenkovGeneratorAction.hh similarity index 84% rename from src/celeritas/optical/detail/CerenkovGeneratorAction.hh rename to src/celeritas/optical/detail/CherenkovGeneratorAction.hh index 461e36d74b..137e9f7b68 100644 --- a/src/celeritas/optical/detail/CerenkovGeneratorAction.hh +++ b/src/celeritas/optical/detail/CherenkovGeneratorAction.hh @@ -3,7 +3,7 @@ // See the top-level COPYRIGHT file for details. // SPDX-License-Identifier: (Apache-2.0 OR MIT) //---------------------------------------------------------------------------// -//! \file celeritas/optical/detail/CerenkovGeneratorAction.hh +//! \file celeritas/optical/detail/CherenkovGeneratorAction.hh //---------------------------------------------------------------------------// #pragma once @@ -19,7 +19,7 @@ namespace celeritas { namespace optical { -class CerenkovParams; +class CherenkovParams; class MaterialParams; } // namespace optical @@ -28,20 +28,20 @@ namespace detail class OffloadParams; //---------------------------------------------------------------------------// /*! - * Generate Cerenkov photons from optical distribution data. + * Generate Cherenkov photons from optical distribution data. * * This samples and buffers new optical track initializers in a reproducible * way. Rather than let each thread generate all initializers from one * distribution, the work is split as evenly as possible among threads: * multiple threads may generate initializers from a single distribution. */ -class CerenkovGeneratorAction final : public CoreStepActionInterface +class CherenkovGeneratorAction final : public CoreStepActionInterface { public: //!@{ //! \name Type aliases - using SPConstCerenkov - = std::shared_ptr; + using SPConstCherenkov + = std::shared_ptr; using SPConstMaterial = std::shared_ptr; using SPOffloadParams = std::shared_ptr; @@ -49,11 +49,11 @@ class CerenkovGeneratorAction final : public CoreStepActionInterface public: // Construct with action ID, data IDs, and optical properties - CerenkovGeneratorAction(ActionId id, + CherenkovGeneratorAction(ActionId id, AuxId offload_id, AuxId optical_id, SPConstMaterial material, - SPConstCerenkov cerenkov, + SPConstCherenkov cherenkov, size_type auto_flush); // Launch kernel with host data @@ -68,7 +68,7 @@ class CerenkovGeneratorAction final : public CoreStepActionInterface //! Short name for the action std::string_view label() const final { - return "generate-cerenkov-photons"; + return "generate-cherenkov-photons"; } // Name of the action (for user output) @@ -84,7 +84,7 @@ class CerenkovGeneratorAction final : public CoreStepActionInterface AuxId offload_id_; AuxId optical_id_; SPConstMaterial material_; - SPConstCerenkov cerenkov_; + SPConstCherenkov cherenkov_; size_type auto_flush_; //// HELPER FUNCTIONS //// diff --git a/src/celeritas/optical/detail/CerenkovGeneratorExecutor.hh b/src/celeritas/optical/detail/CherenkovGeneratorExecutor.hh similarity index 80% rename from src/celeritas/optical/detail/CerenkovGeneratorExecutor.hh rename to src/celeritas/optical/detail/CherenkovGeneratorExecutor.hh index 6f30e81790..2247fb304a 100644 --- a/src/celeritas/optical/detail/CerenkovGeneratorExecutor.hh +++ b/src/celeritas/optical/detail/CherenkovGeneratorExecutor.hh @@ -3,7 +3,7 @@ // See the top-level COPYRIGHT file for details. // SPDX-License-Identifier: (Apache-2.0 OR MIT) //---------------------------------------------------------------------------// -//! \file celeritas/optical/detail/CerenkovGeneratorExecutor.hh +//! \file celeritas/optical/detail/CherenkovGeneratorExecutor.hh //---------------------------------------------------------------------------// #pragma once @@ -11,7 +11,7 @@ #include "corecel/Types.hh" #include "corecel/math/Algorithms.hh" #include "celeritas/global/CoreTrackView.hh" -#include "celeritas/optical/CerenkovGenerator.hh" +#include "celeritas/optical/CherenkovGenerator.hh" #include "celeritas/optical/OffloadData.hh" #include "celeritas/track/CoreStateCounters.hh" @@ -25,15 +25,15 @@ namespace detail // LAUNCHER //---------------------------------------------------------------------------// /*! - * Generate Cerenkov photons from optical distribution data. + * Generate Cherenkov photons from optical distribution data. */ -struct CerenkovGeneratorExecutor +struct CherenkovGeneratorExecutor { //// DATA //// RefPtr state; NativeCRef const material; - NativeCRef const cerenkov; + NativeCRef const cherenkov; NativeRef const offload_state; RefPtr optical_state; OffloadBufferSize size; @@ -49,17 +49,17 @@ struct CerenkovGeneratorExecutor // INLINE DEFINITIONS //---------------------------------------------------------------------------// /*! - * Generate Cerenkov photons from optical distribution data. + * Generate Cherenkov photons from optical distribution data. */ CELER_FUNCTION void -CerenkovGeneratorExecutor::operator()(CoreTrackView const& track) const +CherenkovGeneratorExecutor::operator()(CoreTrackView const& track) const { CELER_EXPECT(state); - CELER_EXPECT(cerenkov); + CELER_EXPECT(cherenkov); CELER_EXPECT(material); CELER_EXPECT(offload_state); CELER_EXPECT(optical_state); - CELER_EXPECT(size.cerenkov <= offload_state.cerenkov.size()); + CELER_EXPECT(size.cherenkov <= offload_state.cherenkov.size()); using DistId = ItemId; using InitId = ItemId; @@ -68,7 +68,7 @@ CerenkovGeneratorExecutor::operator()(CoreTrackView const& track) const // Each bin gives the range of thread IDs that will generate from the // corresponding distribution auto offsets = offload_state.offsets[ItemRange( - ItemId(0), ItemId(size.cerenkov))]; + ItemId(0), ItemId(size.cherenkov))]; // Get the total number of initializers to generate size_type total_work = offsets.back(); @@ -86,13 +86,13 @@ CerenkovGeneratorExecutor::operator()(CoreTrackView const& track) const // Find the distribution this thread will generate from size_type dist_idx = find_distribution_index(offsets, idx); - CELER_ASSERT(dist_idx < size.cerenkov); - auto const& dist = offload_state.cerenkov[DistId(dist_idx)]; + CELER_ASSERT(dist_idx < size.cherenkov); + auto const& dist = offload_state.cherenkov[DistId(dist_idx)]; CELER_ASSERT(dist); // Generate one primary from the distribution optical::MaterialView opt_mat{material, dist.material}; - celeritas::optical::CerenkovGenerator generate(opt_mat, cerenkov, dist); + celeritas::optical::CherenkovGenerator generate(opt_mat, cherenkov, dist); size_type init_idx = counters.num_initializers + idx; CELER_ASSERT(init_idx < optical_state->init.initializers.size()); optical_state->init.initializers[InitId(init_idx)] = generate(rng); diff --git a/src/celeritas/optical/detail/CerenkovOffloadAction.cc b/src/celeritas/optical/detail/CherenkovOffloadAction.cc similarity index 77% rename from src/celeritas/optical/detail/CerenkovOffloadAction.cc rename to src/celeritas/optical/detail/CherenkovOffloadAction.cc index f68993ff50..ab16f4f105 100644 --- a/src/celeritas/optical/detail/CerenkovOffloadAction.cc +++ b/src/celeritas/optical/detail/CherenkovOffloadAction.cc @@ -3,9 +3,9 @@ // See the top-level COPYRIGHT file for details. // SPDX-License-Identifier: (Apache-2.0 OR MIT) //---------------------------------------------------------------------------// -//! \file celeritas/optical/detail/CerenkovOffloadAction.cc +//! \file celeritas/optical/detail/CherenkovOffloadAction.cc //---------------------------------------------------------------------------// -#include "CerenkovOffloadAction.hh" +#include "CherenkovOffloadAction.hh" #include @@ -16,10 +16,10 @@ #include "celeritas/global/CoreState.hh" #include "celeritas/global/CoreTrackData.hh" #include "celeritas/global/TrackExecutor.hh" -#include "celeritas/optical/CerenkovParams.hh" +#include "celeritas/optical/CherenkovParams.hh" #include "celeritas/optical/MaterialParams.hh" -#include "CerenkovOffloadExecutor.hh" +#include "CherenkovOffloadExecutor.hh" #include "OffloadParams.hh" #include "OpticalGenAlgorithms.hh" @@ -31,34 +31,34 @@ namespace detail /*! * Construct with action ID, data ID, optical material. */ -CerenkovOffloadAction::CerenkovOffloadAction(ActionId id, +CherenkovOffloadAction::CherenkovOffloadAction(ActionId id, AuxId data_id, SPConstMaterial material, - SPConstCerenkov cerenkov) + SPConstCherenkov cherenkov) : id_(id) , data_id_{data_id} , material_(std::move(material)) - , cerenkov_(std::move(cerenkov)) + , cherenkov_(std::move(cherenkov)) { CELER_EXPECT(id_); CELER_EXPECT(data_id_); - CELER_EXPECT(cerenkov_ && material_); + CELER_EXPECT(cherenkov_ && material_); } //---------------------------------------------------------------------------// /*! * Descriptive name of the action. */ -std::string_view CerenkovOffloadAction::description() const +std::string_view CherenkovOffloadAction::description() const { - return "generate Cerenkov optical distribution data"; + return "generate Cherenkov optical distribution data"; } //---------------------------------------------------------------------------// /*! * Execute the action with host data. */ -void CerenkovOffloadAction::step(CoreParams const& params, +void CherenkovOffloadAction::step(CoreParams const& params, CoreStateHost& state) const { this->step_impl(params, state); @@ -68,7 +68,7 @@ void CerenkovOffloadAction::step(CoreParams const& params, /*! * Execute the action with device data. */ -void CerenkovOffloadAction::step(CoreParams const& params, +void CherenkovOffloadAction::step(CoreParams const& params, CoreStateDevice& state) const { this->step_impl(params, state); @@ -79,16 +79,16 @@ void CerenkovOffloadAction::step(CoreParams const& params, * Generate optical distribution data post-step. */ template -void CerenkovOffloadAction::step_impl(CoreParams const& core_params, +void CherenkovOffloadAction::step_impl(CoreParams const& core_params, CoreState& core_state) const { auto& state = get>(core_state.aux(), data_id_); - auto& buffer = state.store.ref().cerenkov; - auto& buffer_size = state.buffer_size.cerenkov; + auto& buffer = state.store.ref().cherenkov; + auto& buffer_size = state.buffer_size.cherenkov; CELER_VALIDATE(buffer_size + core_state.size() <= buffer.size(), << "insufficient capacity (" << buffer.size() - << ") for buffered Cerenkov distribution data (total " + << ") for buffered Cherenkov distribution data (total " "capacity requirement of " << buffer_size + core_state.size() << ")"); @@ -110,7 +110,7 @@ void CerenkovOffloadAction::step_impl(CoreParams const& core_params, /*! * Launch a (host) kernel to generate optical distribution data post-step. */ -void CerenkovOffloadAction::pre_generate(CoreParams const& core_params, +void CherenkovOffloadAction::pre_generate(CoreParams const& core_params, CoreStateHost& core_state) const { auto& state = get>(core_state.aux(), @@ -118,8 +118,8 @@ void CerenkovOffloadAction::pre_generate(CoreParams const& core_params, TrackExecutor execute{core_params.ptr(), core_state.ptr(), - detail::CerenkovOffloadExecutor{material_->host_ref(), - cerenkov_->host_ref(), + detail::CherenkovOffloadExecutor{material_->host_ref(), + cherenkov_->host_ref(), state.store.ref(), state.buffer_size}}; launch_action(*this, core_params, core_state, execute); @@ -127,7 +127,7 @@ void CerenkovOffloadAction::pre_generate(CoreParams const& core_params, //---------------------------------------------------------------------------// #if !CELER_USE_DEVICE -void CerenkovOffloadAction::pre_generate(CoreParams const&, +void CherenkovOffloadAction::pre_generate(CoreParams const&, CoreStateDevice&) const { CELER_NOT_CONFIGURED("CUDA OR HIP"); diff --git a/src/celeritas/optical/detail/CerenkovOffloadAction.cu b/src/celeritas/optical/detail/CherenkovOffloadAction.cu similarity index 80% rename from src/celeritas/optical/detail/CerenkovOffloadAction.cu rename to src/celeritas/optical/detail/CherenkovOffloadAction.cu index 8189c10cb7..7251f40d56 100644 --- a/src/celeritas/optical/detail/CerenkovOffloadAction.cu +++ b/src/celeritas/optical/detail/CherenkovOffloadAction.cu @@ -3,9 +3,9 @@ // See the top-level COPYRIGHT file for details. // SPDX-License-Identifier: (Apache-2.0 OR MIT) //---------------------------------------------------------------------------// -//! \file celeritas/optical/detail/CerenkovOffloadAction.cu +//! \file celeritas/optical/detail/CherenkovOffloadAction.cu //---------------------------------------------------------------------------// -#include "CerenkovOffloadAction.hh" +#include "CherenkovOffloadAction.hh" #include "corecel/Assert.hh" #include "corecel/sys/ScopedProfiling.hh" @@ -13,10 +13,10 @@ #include "celeritas/global/CoreParams.hh" #include "celeritas/global/CoreState.hh" #include "celeritas/global/TrackExecutor.hh" -#include "celeritas/optical/CerenkovParams.hh" +#include "celeritas/optical/CherenkovParams.hh" #include "celeritas/optical/MaterialParams.hh" -#include "CerenkovOffloadExecutor.hh" +#include "CherenkovOffloadExecutor.hh" #include "OffloadParams.hh" #include "OpticalGenAlgorithms.hh" @@ -28,7 +28,7 @@ namespace detail /*! * Launch a kernel to generate optical distribution data post-step. */ -void CerenkovOffloadAction::pre_generate(CoreParams const& core_params, +void CherenkovOffloadAction::pre_generate(CoreParams const& core_params, CoreStateDevice& core_state) const { auto& state = get>(core_state.aux(), @@ -36,8 +36,8 @@ void CerenkovOffloadAction::pre_generate(CoreParams const& core_params, TrackExecutor execute{ core_params.ptr(), core_state.ptr(), - detail::CerenkovOffloadExecutor{material_->device_ref(), - cerenkov_->device_ref(), + detail::CherenkovOffloadExecutor{material_->device_ref(), + cherenkov_->device_ref(), state.store.ref(), state.buffer_size}}; static ActionLauncher const launch_kernel(*this); diff --git a/src/celeritas/optical/detail/CerenkovOffloadAction.hh b/src/celeritas/optical/detail/CherenkovOffloadAction.hh similarity index 83% rename from src/celeritas/optical/detail/CerenkovOffloadAction.hh rename to src/celeritas/optical/detail/CherenkovOffloadAction.hh index 8a1a222f25..0ceec7fd72 100644 --- a/src/celeritas/optical/detail/CerenkovOffloadAction.hh +++ b/src/celeritas/optical/detail/CherenkovOffloadAction.hh @@ -3,7 +3,7 @@ // See the top-level COPYRIGHT file for details. // SPDX-License-Identifier: (Apache-2.0 OR MIT) //---------------------------------------------------------------------------// -//! \file celeritas/optical/detail/CerenkovOffloadAction.hh +//! \file celeritas/optical/detail/CherenkovOffloadAction.hh //---------------------------------------------------------------------------// #pragma once @@ -21,7 +21,7 @@ namespace celeritas { namespace optical { -class CerenkovParams; +class CherenkovParams; class MaterialParams; } // namespace optical @@ -31,23 +31,23 @@ namespace detail /*! * Generate optical distribution data. */ -class CerenkovOffloadAction final : public CoreStepActionInterface +class CherenkovOffloadAction final : public CoreStepActionInterface { public: //!@{ //! \name Type aliases - using SPConstCerenkov - = std::shared_ptr; + using SPConstCherenkov + = std::shared_ptr; using SPConstMaterial = std::shared_ptr; //!@} public: // Construct with action ID, optical material, and storage - CerenkovOffloadAction(ActionId id, + CherenkovOffloadAction(ActionId id, AuxId data_id, SPConstMaterial material, - SPConstCerenkov cerenkov); + SPConstCherenkov cherenkov); // Launch kernel with host data void step(CoreParams const&, CoreStateHost&) const final; @@ -59,7 +59,7 @@ class CerenkovOffloadAction final : public CoreStepActionInterface ActionId action_id() const final { return id_; } //! Short name for the action - std::string_view label() const final { return "cerenkov-offload"; } + std::string_view label() const final { return "cherenkov-offload"; } // Name of the action (for user output) std::string_view description() const final; @@ -73,7 +73,7 @@ class CerenkovOffloadAction final : public CoreStepActionInterface ActionId id_; AuxId data_id_; SPConstMaterial material_; - SPConstCerenkov cerenkov_; + SPConstCherenkov cherenkov_; //// HELPER FUNCTIONS //// diff --git a/src/celeritas/optical/detail/CerenkovOffloadExecutor.hh b/src/celeritas/optical/detail/CherenkovOffloadExecutor.hh similarity index 76% rename from src/celeritas/optical/detail/CerenkovOffloadExecutor.hh rename to src/celeritas/optical/detail/CherenkovOffloadExecutor.hh index 645aafad01..791c98b087 100644 --- a/src/celeritas/optical/detail/CerenkovOffloadExecutor.hh +++ b/src/celeritas/optical/detail/CherenkovOffloadExecutor.hh @@ -3,14 +3,14 @@ // See the top-level COPYRIGHT file for details. // SPDX-License-Identifier: (Apache-2.0 OR MIT) //---------------------------------------------------------------------------// -//! \file celeritas/optical/detail/CerenkovOffloadExecutor.hh +//! \file celeritas/optical/detail/CherenkovOffloadExecutor.hh //---------------------------------------------------------------------------// #pragma once #include "corecel/Macros.hh" #include "corecel/Types.hh" #include "celeritas/global/CoreTrackView.hh" -#include "celeritas/optical/CerenkovOffload.hh" +#include "celeritas/optical/CherenkovOffload.hh" #include "celeritas/optical/OffloadData.hh" namespace celeritas @@ -23,13 +23,13 @@ namespace detail /*! * Generate optical distribution data. */ -struct CerenkovOffloadExecutor +struct CherenkovOffloadExecutor { inline CELER_FUNCTION void operator()(celeritas::CoreTrackView const& track); NativeCRef const material; - NativeCRef const cerenkov; + NativeCRef const cherenkov; NativeRef const state; OffloadBufferSize size; }; @@ -41,20 +41,20 @@ struct CerenkovOffloadExecutor * Generate optical distribution data. */ CELER_FUNCTION void -CerenkovOffloadExecutor::operator()(CoreTrackView const& track) +CherenkovOffloadExecutor::operator()(CoreTrackView const& track) { CELER_EXPECT(state); - CELER_EXPECT(cerenkov); + CELER_EXPECT(cherenkov); CELER_EXPECT(material); using DistId = ItemId; auto tsid = track.track_slot_id(); - CELER_ASSERT(size.cerenkov + tsid.get() < state.cerenkov.size()); - auto& cerenkov_dist = state.cerenkov[DistId(size.cerenkov + tsid.get())]; + CELER_ASSERT(size.cherenkov + tsid.get() < state.cherenkov.size()); + auto& cherenkov_dist = state.cherenkov[DistId(size.cherenkov + tsid.get())]; // Clear distribution data - cerenkov_dist = {}; + cherenkov_dist = {}; auto sim = track.make_sim_view(); auto const& step = state.step[tsid]; @@ -68,15 +68,15 @@ CerenkovOffloadExecutor::operator()(CoreTrackView const& track) auto particle = track.make_particle_view(); - // Get the distribution data used to generate Cerenkov optical photons + // Get the distribution data used to generate Cherenkov optical photons if (particle.charge() != zero_quantity()) { Real3 const& pos = track.make_geo_view().pos(); optical::MaterialView opt_mat{material, step.material}; auto rng = track.make_rng_engine(); - CerenkovOffload generate(particle, sim, opt_mat, pos, cerenkov, step); - cerenkov_dist = generate(rng); + CherenkovOffload generate(particle, sim, opt_mat, pos, cherenkov, step); + cherenkov_dist = generate(rng); } } diff --git a/test/celeritas/CMakeLists.txt b/test/celeritas/CMakeLists.txt index 6c68d51874..af009b4047 100644 --- a/test/celeritas/CMakeLists.txt +++ b/test/celeritas/CMakeLists.txt @@ -305,7 +305,7 @@ celeritas_add_test(io/SeltzerBergerReader.test.cc ${_needs_geant4}) #-----------------------------------------------------------------------------# # Optical -celeritas_add_test(optical/Cerenkov.test.cc) +celeritas_add_test(optical/Cherenkov.test.cc) celeritas_add_test(optical/OpticalCollector.test.cc ${_needs_geant4}) celeritas_add_test(optical/OpticalUtils.test.cc) celeritas_add_test(optical/Scintillation.test.cc) diff --git a/test/celeritas/GlobalTestBase.hh b/test/celeritas/GlobalTestBase.hh index 6af0633809..5eba065a06 100644 --- a/test/celeritas/GlobalTestBase.hh +++ b/test/celeritas/GlobalTestBase.hh @@ -47,7 +47,7 @@ struct Primary; namespace optical { -class CerenkovParams; +class CherenkovParams; class MaterialParams; class ScintillationParams; } // namespace optical @@ -87,7 +87,7 @@ class GlobalTestBase : public Test using SPOutputRegistry = SP; using SPUserRegistry = SP; - using SPConstCerenkov = SP; + using SPConstCherenkov = SP; using SPConstOpticalMaterial = SP; using SPConstScintillation = SP; @@ -119,7 +119,7 @@ class GlobalTestBase : public Test inline SPActionRegistry const& action_reg(); inline SPUserRegistry const& aux_reg(); inline SPConstCore const& core(); - inline SPConstCerenkov const& cerenkov(); + inline SPConstCherenkov const& cherenkov(); inline SPConstOpticalMaterial const& optical_material(); inline SPConstScintillation const& scintillation(); @@ -137,7 +137,7 @@ class GlobalTestBase : public Test inline SPActionRegistry const& action_reg() const; inline SPUserRegistry const& aux_reg() const; inline SPConstCore const& core() const; - inline SPConstCerenkov const& cerenkov() const; + inline SPConstCherenkov const& cherenkov() const; inline SPConstOpticalMaterial const& optical_material() const; inline SPConstScintillation const& scintillation() const; //!@} @@ -164,7 +164,7 @@ class GlobalTestBase : public Test [[nodiscard]] virtual SPConstTrackInit build_init() = 0; [[nodiscard]] virtual SPConstWentzelOKVI build_wentzel() = 0; [[nodiscard]] virtual SPConstAction build_along_step() = 0; - [[nodiscard]] virtual SPConstCerenkov build_cerenkov() = 0; + [[nodiscard]] virtual SPConstCherenkov build_cherenkov() = 0; [[nodiscard]] virtual SPConstOpticalMaterial build_optical_material() = 0; [[nodiscard]] virtual SPConstScintillation build_scintillation() = 0; @@ -193,7 +193,7 @@ class GlobalTestBase : public Test SPConstWentzelOKVI wentzel_; SPConstCore core_; SPOutputRegistry output_reg_; - SPConstCerenkov cerenkov_; + SPConstCherenkov cherenkov_; SPConstOpticalMaterial optical_material_; SPConstScintillation scintillation_; @@ -234,7 +234,7 @@ DEF_GTB_ACCESSORS(SPConstTrackInit, init) DEF_GTB_ACCESSORS(SPActionRegistry, action_reg) DEF_GTB_ACCESSORS(SPUserRegistry, aux_reg) DEF_GTB_ACCESSORS(SPConstCore, core) -DEF_GTB_ACCESSORS(SPConstCerenkov, cerenkov) +DEF_GTB_ACCESSORS(SPConstCherenkov, cherenkov) DEF_GTB_ACCESSORS(SPConstOpticalMaterial, optical_material) DEF_GTB_ACCESSORS(SPConstScintillation, scintillation) auto GlobalTestBase::wentzel() -> SPConstWentzelOKVI const& diff --git a/test/celeritas/ImportedDataTestBase.cc b/test/celeritas/ImportedDataTestBase.cc index fa9b44d21d..7169977ff2 100644 --- a/test/celeritas/ImportedDataTestBase.cc +++ b/test/celeritas/ImportedDataTestBase.cc @@ -11,7 +11,7 @@ #include "celeritas/geo/GeoMaterialParams.hh" #include "celeritas/io/ImportData.hh" #include "celeritas/mat/MaterialParams.hh" -#include "celeritas/optical/CerenkovParams.hh" +#include "celeritas/optical/CherenkovParams.hh" #include "celeritas/optical/MaterialParams.hh" #include "celeritas/optical/ScintillationParams.hh" #include "celeritas/phys/CutoffParams.hh" @@ -133,9 +133,9 @@ auto ImportedDataTestBase::build_physics() -> SPConstPhysics } //---------------------------------------------------------------------------// -auto ImportedDataTestBase::build_cerenkov() -> SPConstCerenkov +auto ImportedDataTestBase::build_cherenkov() -> SPConstCherenkov { - return std::make_shared(this->optical_material()); + return std::make_shared(this->optical_material()); } //---------------------------------------------------------------------------// diff --git a/test/celeritas/ImportedDataTestBase.hh b/test/celeritas/ImportedDataTestBase.hh index bd98108bf5..57b0ae33f4 100644 --- a/test/celeritas/ImportedDataTestBase.hh +++ b/test/celeritas/ImportedDataTestBase.hh @@ -51,7 +51,7 @@ class ImportedDataTestBase : virtual public GlobalGeoTestBase SPConstPhysics build_physics() override; SPConstSim build_sim() override; SPConstWentzelOKVI build_wentzel() override; - SPConstCerenkov build_cerenkov() override; + SPConstCherenkov build_cherenkov() override; SPConstOpticalMaterial build_optical_material() override; SPConstScintillation build_scintillation() override; }; diff --git a/test/celeritas/OnlyCoreTestBase.hh b/test/celeritas/OnlyCoreTestBase.hh index 72b1592806..547804e4bd 100644 --- a/test/celeritas/OnlyCoreTestBase.hh +++ b/test/celeritas/OnlyCoreTestBase.hh @@ -23,7 +23,7 @@ namespace test class OnlyCoreTestBase : virtual public GlobalTestBase { public: - SPConstCerenkov build_cerenkov() override { CELER_ASSERT_UNREACHABLE(); } + SPConstCherenkov build_cherenkov() override { CELER_ASSERT_UNREACHABLE(); } SPConstOpticalMaterial build_optical_material() override { CELER_ASSERT_UNREACHABLE(); diff --git a/test/celeritas/data/lar-sphere.geant.json b/test/celeritas/data/lar-sphere.geant.json index fabbb5c11e..e98f17ffc2 100644 --- a/test/celeritas/data/lar-sphere.geant.json +++ b/test/celeritas/data/lar-sphere.geant.json @@ -47,7 +47,7 @@ "relaxation": "none", "verbose": false, "optical": { - "cerenkov": { "enable": false }, + "cherenkov": { "enable": false }, "scintillation": { "enable": false }, "wavelength_shifting": "delta", "wavelength_shifting2": "none", diff --git a/test/celeritas/optical/Cerenkov.test.cc b/test/celeritas/optical/Cherenkov.test.cc similarity index 95% rename from test/celeritas/optical/Cerenkov.test.cc rename to test/celeritas/optical/Cherenkov.test.cc index 8454a9067f..ca56813a85 100644 --- a/test/celeritas/optical/Cerenkov.test.cc +++ b/test/celeritas/optical/Cherenkov.test.cc @@ -3,7 +3,7 @@ // See the top-level COPYRIGHT file for details. // SPDX-License-Identifier: (Apache-2.0 OR MIT) //---------------------------------------------------------------------------// -//! \file celeritas/optical/Cerenkov.test.cc +//! \file celeritas/optical/Cherenkov.test.cc //---------------------------------------------------------------------------// #include #include @@ -20,10 +20,10 @@ #include "celeritas/Constants.hh" #include "celeritas/Units.hh" #include "celeritas/io/ImportOpticalMaterial.hh" -#include "celeritas/optical/CerenkovDndxCalculator.hh" -#include "celeritas/optical/CerenkovGenerator.hh" -#include "celeritas/optical/CerenkovOffload.hh" -#include "celeritas/optical/CerenkovParams.hh" +#include "celeritas/optical/CherenkovDndxCalculator.hh" +#include "celeritas/optical/CherenkovGenerator.hh" +#include "celeritas/optical/CherenkovOffload.hh" +#include "celeritas/optical/CherenkovParams.hh" #include "celeritas/optical/GeneratorDistributionData.hh" #include "celeritas/optical/MaterialParams.hh" #include "celeritas/optical/detail/OpticalUtils.hh" @@ -125,7 +125,7 @@ real_type um_to_mev(real_type wavelength_um) // TEST HARNESS //---------------------------------------------------------------------------// -class CerenkovTest : public ::celeritas::test::OpticalTestBase +class CherenkovTest : public ::celeritas::test::OpticalTestBase { protected: using Energy = units::MevEnergy; @@ -148,12 +148,12 @@ class CerenkovTest : public ::celeritas::test::OpticalTestBase input.volume_to_mat = {OpticalMaterialId{0}}; material = std::make_shared(std::move(input)); - // Build Cerenkov data - params = std::make_shared(material); + // Build Cherenkov data + params = std::make_shared(material); } std::shared_ptr material; - std::shared_ptr params; + std::shared_ptr params; OpticalMaterialId material_id{0}; }; @@ -161,7 +161,7 @@ class CerenkovTest : public ::celeritas::test::OpticalTestBase // TESTS //---------------------------------------------------------------------------// -TEST_F(CerenkovTest, angle_integral) +TEST_F(CherenkovTest, angle_integral) { // Check conversion: 1 μm wavelength is approximately 1.2398 eV EXPECT_SOFT_EQ(1.2398419843320026e-6, um_to_mev(1)); @@ -181,7 +181,7 @@ TEST_F(CerenkovTest, angle_integral) //---------------------------------------------------------------------------// -TEST_F(CerenkovTest, dndx) +TEST_F(CherenkovTest, dndx) { EXPECT_SOFT_NEAR(369.81e6, constants::alpha_fine_structure * units::Mev::value() @@ -190,7 +190,7 @@ TEST_F(CerenkovTest, dndx) 1e-6); MaterialView mat_view{material->host_ref(), material_id}; - CerenkovDndxCalculator calc_dndx( + CherenkovDndxCalculator calc_dndx( mat_view, params->host_ref(), this->particle_params()->get(ParticleId{0}).charge()); @@ -221,7 +221,7 @@ TEST_F(CerenkovTest, dndx) //---------------------------------------------------------------------------// -TEST_F(CerenkovTest, TEST_IF_CELERITAS_DOUBLE(pre_generator)) +TEST_F(CherenkovTest, TEST_IF_CELERITAS_DOUBLE(pre_generator)) { Rng rng; MaterialView const mat_view{material->host_ref(), material_id}; @@ -241,7 +241,7 @@ TEST_F(CerenkovTest, TEST_IF_CELERITAS_DOUBLE(pre_generator)) auto sim = this->make_sim_track_view(0.15); Real3 pos = {sim.step_length(), 0, 0}; - CerenkovOffload pre_generate( + CherenkovOffload pre_generate( particle, sim, mat_view, pos, params->host_ref(), pre_step); size_type num_samples = 10; @@ -271,7 +271,7 @@ TEST_F(CerenkovTest, TEST_IF_CELERITAS_DOUBLE(pre_generator)) EXPECT_VEC_EQ(expected_num_photons, sampled_num_photons); } - // Below Cerenkov threshold + // Below Cherenkov threshold { // Pre-step values OffloadPreStepData pre_step; @@ -286,7 +286,7 @@ TEST_F(CerenkovTest, TEST_IF_CELERITAS_DOUBLE(pre_generator)) auto sim = this->make_sim_track_view(0.1); Real3 pos = {sim.step_length(), 0, 0}; - CerenkovOffload pre_generate( + CherenkovOffload pre_generate( particle, sim, mat_view, pos, params->host_ref(), pre_step); auto const result = pre_generate(rng); @@ -297,7 +297,7 @@ TEST_F(CerenkovTest, TEST_IF_CELERITAS_DOUBLE(pre_generator)) //---------------------------------------------------------------------------// -TEST_F(CerenkovTest, TEST_IF_CELERITAS_DOUBLE(generator)) +TEST_F(CherenkovTest, TEST_IF_CELERITAS_DOUBLE(generator)) { Rng rng; MaterialView mat_view{material->host_ref(), material_id}; @@ -338,7 +338,7 @@ TEST_F(CerenkovTest, TEST_IF_CELERITAS_DOUBLE(generator)) real_type ddel = (dmax - dmin) / num_bins; // Calculate the average number of photons produced per unit length - CerenkovOffload pre_generate( + CherenkovOffload pre_generate( particle, sim, mat_view, pos, params->host_ref(), pre_step); Real3 inc_dir = make_unit_vector(pos - pre_step.pos); @@ -348,7 +348,7 @@ TEST_F(CerenkovTest, TEST_IF_CELERITAS_DOUBLE(generator)) CELER_ASSERT(dist); // Sample the optical photons - CerenkovGenerator generate_photon( + CherenkovGenerator generate_photon( mat_view, params->host_ref(), dist); for (size_type j = 0; j < dist.num_photons; ++j) diff --git a/test/celeritas/optical/OpticalCollector.test.cc b/test/celeritas/optical/OpticalCollector.test.cc index db58cebef3..52b1ccf02d 100644 --- a/test/celeritas/optical/OpticalCollector.test.cc +++ b/test/celeritas/optical/OpticalCollector.test.cc @@ -62,7 +62,7 @@ class LArSphereOffloadTest : public LArSphereBase { // Optical distribution data size_type num_photons{0}; - OffloadResult cerenkov; + OffloadResult cherenkov; OffloadResult scintillation; // Step iteration at which the optical tracking loop launched @@ -92,7 +92,7 @@ class LArSphereOffloadTest : public LArSphereBase // Optical collector options bool use_scintillation_{true}; - bool use_cerenkov_{true}; + bool use_cherenkov_{true}; size_type num_track_slots_{4096}; size_type buffer_capacity_{256}; size_type initializer_capacity_{8192}; @@ -111,21 +111,21 @@ void LArSphereOffloadTest::RunResult::print_expected() const << this->num_photons << ", result.num_photons);\n" "EXPECT_EQ(" - << this->cerenkov.total_num_photons - << ", result.cerenkov.total_num_photons);\n" + << this->cherenkov.total_num_photons + << ", result.cherenkov.total_num_photons);\n" "EXPECT_EQ(" - << this->cerenkov.num_photons.size() - << ", result.cerenkov.num_photons.size());\n" - "static size_type const expected_cerenkov_num_photons[] = " - << repr(this->cerenkov.num_photons) + << this->cherenkov.num_photons.size() + << ", result.cherenkov.num_photons.size());\n" + "static size_type const expected_cherenkov_num_photons[] = " + << repr(this->cherenkov.num_photons) << ";\n" - "EXPECT_VEC_EQ(expected_cerenkov_num_photons, " - "result.cerenkov.num_photons);\n" - "static real_type const expected_cerenkov_charge[] = " - << repr(this->cerenkov.charge) + "EXPECT_VEC_EQ(expected_cherenkov_num_photons, " + "result.cherenkov.num_photons);\n" + "static real_type const expected_cherenkov_charge[] = " + << repr(this->cherenkov.charge) << ";\n" - "EXPECT_VEC_EQ(expected_cerenkov_charge, " - "result.cerenkov.charge);\n" + "EXPECT_VEC_EQ(expected_cherenkov_charge, " + "result.cherenkov.charge);\n" "EXPECT_EQ(" << this->scintillation.total_num_photons << ", result.scintillation.total_num_photons);\n" @@ -173,9 +173,9 @@ void LArSphereOffloadTest::build_optical_collector() { OpticalCollector::Input inp; inp.material = this->optical_material(); - if (use_cerenkov_) + if (use_cherenkov_) { - inp.cerenkov = this->cerenkov(); + inp.cherenkov = this->cherenkov(); } if (use_scintillation_) { @@ -292,7 +292,7 @@ auto LArSphereOffloadTest::run(size_type num_primaries, auto const& state = offload_state.store.ref(); auto const& sizes = offload_state.buffer_size; - get_result(result.cerenkov, state.cerenkov, sizes.cerenkov); + get_result(result.cherenkov, state.cherenkov, sizes.cherenkov); get_result(result.scintillation, state.scintillation, sizes.scintillation); result.num_photons = sizes.num_photons; @@ -317,28 +317,28 @@ TEST_F(LArSphereOffloadTest, host_distributions) auto result = this->run(4, num_track_slots_, 64); - EXPECT_EQ(result.cerenkov.total_num_photons + EXPECT_EQ(result.cherenkov.total_num_photons + result.scintillation.total_num_photons, result.num_photons); - static real_type const expected_cerenkov_charge[] = {-1, 1}; - EXPECT_VEC_EQ(expected_cerenkov_charge, result.cerenkov.charge); + static real_type const expected_cherenkov_charge[] = {-1, 1}; + EXPECT_VEC_EQ(expected_cherenkov_charge, result.cherenkov.charge); static real_type const expected_scintillation_charge[] = {-1, 0, 1}; EXPECT_VEC_EQ(expected_scintillation_charge, result.scintillation.charge); if (CELERITAS_REAL_TYPE == CELERITAS_REAL_TYPE_DOUBLE) { - EXPECT_EQ(23492, result.cerenkov.total_num_photons); - EXPECT_EQ(48, result.cerenkov.num_photons.size()); - static size_type const expected_cerenkov_num_photons[] + EXPECT_EQ(23492, result.cherenkov.total_num_photons); + EXPECT_EQ(48, result.cherenkov.num_photons.size()); + static size_type const expected_cherenkov_num_photons[] = {337u, 503u, 1532u, 1485u, 788u, 610u, 1271u, 433u, 912u, 1051u, 756u, 1124u, 796u, 854u, 446u, 420u, 582u, 648u, 704u, 825u, 419u, 496u, 520u, 213u, 338u, 376u, 391u, 517u, 238u, 270u, 254u, 370u, 23u, 115u, 129u, 317u, 183u, 10u, 1u, 431u, 301u, 500u, 187u, 373u, 20u, 277u, 145u, 1u}; - EXPECT_VEC_EQ(expected_cerenkov_num_photons, - result.cerenkov.num_photons); + EXPECT_VEC_EQ(expected_cherenkov_num_photons, + result.cherenkov.num_photons); EXPECT_EQ(2101748, result.scintillation.total_num_photons); EXPECT_EQ(106, result.scintillation.num_photons.size()); @@ -362,8 +362,8 @@ TEST_F(LArSphereOffloadTest, host_distributions) } else { - EXPECT_EQ(21572, result.cerenkov.total_num_photons); - EXPECT_EQ(52, result.cerenkov.num_photons.size()); + EXPECT_EQ(21572, result.cherenkov.total_num_photons); + EXPECT_EQ(52, result.cherenkov.num_photons.size()); EXPECT_EQ(2104145, result.scintillation.total_num_photons); EXPECT_EQ(130, result.scintillation.num_photons.size()); @@ -378,21 +378,21 @@ TEST_F(LArSphereOffloadTest, TEST_IF_CELER_DEVICE(device_distributions)) auto result = this->run(8, num_track_slots_, 32); - EXPECT_EQ(result.cerenkov.total_num_photons + EXPECT_EQ(result.cherenkov.total_num_photons + result.scintillation.total_num_photons, result.num_photons); - static real_type const expected_cerenkov_charge[] = {-1, 1}; - EXPECT_VEC_EQ(expected_cerenkov_charge, result.cerenkov.charge); + static real_type const expected_cherenkov_charge[] = {-1, 1}; + EXPECT_VEC_EQ(expected_cherenkov_charge, result.cherenkov.charge); static real_type const expected_scintillation_charge[] = {-1, 0, 1}; EXPECT_VEC_EQ(expected_scintillation_charge, result.scintillation.charge); if (CELERITAS_REAL_TYPE == CELERITAS_REAL_TYPE_DOUBLE) { - EXPECT_EQ(42082, result.cerenkov.total_num_photons); - EXPECT_EQ(81, result.cerenkov.num_photons.size()); - static size_type const expected_cerenkov_num_photons[] + EXPECT_EQ(42082, result.cherenkov.total_num_photons); + EXPECT_EQ(81, result.cherenkov.num_photons.size()); + static size_type const expected_cherenkov_num_photons[] = {337u, 503u, 1532u, 1485u, 1376u, 1471u, 1153u, 877u, 788u, 610u, 1271u, 433u, 1068u, 1238u, 110u, 705u, 912u, 1051u, 756u, 1124u, 779u, 1014u, 594u, 532u, 796u, 854u, 446u, @@ -402,8 +402,8 @@ TEST_F(LArSphereOffloadTest, TEST_IF_CELER_DEVICE(device_distributions)) 675u, 68u, 238u, 270u, 254u, 370u, 315u, 231u, 461u, 61u, 23u, 115u, 129u, 317u, 188u, 97u, 406u, 183u, 22u, 268u, 10u, 128u, 26u, 153u, 1u, 105u, 2u}; - EXPECT_VEC_EQ(expected_cerenkov_num_photons, - result.cerenkov.num_photons); + EXPECT_VEC_EQ(expected_cherenkov_num_photons, + result.cherenkov.num_photons); EXPECT_EQ(3759163, result.scintillation.total_num_photons); EXPECT_EQ(193, result.scintillation.num_photons.size()); @@ -438,15 +438,15 @@ TEST_F(LArSphereOffloadTest, TEST_IF_CELER_DEVICE(device_distributions)) } else { - EXPECT_EQ(39194, result.cerenkov.total_num_photons); - EXPECT_EQ(81, result.cerenkov.num_photons.size()); + EXPECT_EQ(39194, result.cherenkov.total_num_photons); + EXPECT_EQ(81, result.cherenkov.num_photons.size()); EXPECT_EQ(3595786, result.scintillation.total_num_photons); EXPECT_EQ(196, result.scintillation.num_photons.size()); } } -TEST_F(LArSphereOffloadTest, cerenkov_distributiona) +TEST_F(LArSphereOffloadTest, cherenkov_distributiona) { use_scintillation_ = false; auto_flush_ = size_type(-1); @@ -460,27 +460,27 @@ TEST_F(LArSphereOffloadTest, cerenkov_distributiona) if (CELERITAS_REAL_TYPE == CELERITAS_REAL_TYPE_DOUBLE) { - EXPECT_EQ(19601, result.cerenkov.total_num_photons); - EXPECT_EQ(37, result.cerenkov.num_photons.size()); + EXPECT_EQ(19601, result.cherenkov.total_num_photons); + EXPECT_EQ(37, result.cherenkov.num_photons.size()); } else { - EXPECT_EQ(20790, result.cerenkov.total_num_photons); - EXPECT_EQ(43, result.cerenkov.num_photons.size()); + EXPECT_EQ(20790, result.cherenkov.total_num_photons); + EXPECT_EQ(43, result.cherenkov.num_photons.size()); } } TEST_F(LArSphereOffloadTest, scintillation_distributions) { - use_cerenkov_ = false; + use_cherenkov_ = false; auto_flush_ = size_type(-1); num_track_slots_ = 4; this->build_optical_collector(); auto result = this->run(4, num_track_slots_, 16); - EXPECT_EQ(0, result.cerenkov.total_num_photons); - EXPECT_EQ(0, result.cerenkov.num_photons.size()); + EXPECT_EQ(0, result.cherenkov.total_num_photons); + EXPECT_EQ(0, result.cherenkov.num_photons.size()); if (CELERITAS_REAL_TYPE == CELERITAS_REAL_TYPE_DOUBLE) { @@ -521,7 +521,7 @@ TEST_F(LArSphereOffloadTest, host_generate) EXPECT_EQ(2, result.optical_launch_step); EXPECT_EQ(0, result.scintillation.total_num_photons); - EXPECT_EQ(0, result.cerenkov.total_num_photons); + EXPECT_EQ(0, result.cherenkov.total_num_photons); } TEST_F(LArSphereOffloadTest, TEST_IF_CELER_DEVICE(device_generate)) @@ -540,7 +540,7 @@ TEST_F(LArSphereOffloadTest, TEST_IF_CELER_DEVICE(device_generate)) EXPECT_EQ(7, result.optical_launch_step); EXPECT_EQ(0, result.scintillation.total_num_photons); - EXPECT_EQ(0, result.cerenkov.total_num_photons); + EXPECT_EQ(0, result.cherenkov.total_num_photons); } //---------------------------------------------------------------------------// diff --git a/test/celeritas/optical/OpticalTestBase.cc b/test/celeritas/optical/OpticalTestBase.cc index b68917fbfa..41837a3fe0 100644 --- a/test/celeritas/optical/OpticalTestBase.cc +++ b/test/celeritas/optical/OpticalTestBase.cc @@ -3,7 +3,7 @@ // See the top-level COPYRIGHT file for details. // SPDX-License-Identifier: (Apache-2.0 OR MIT) //---------------------------------------------------------------------------// -//! \file celeritas/optical/Cerenkov.test.cc +//! \file celeritas/optical/Cherenkov.test.cc //---------------------------------------------------------------------------// #include "OpticalTestBase.hh" From a04397833ba569b590fee19458a60c57584a104d Mon Sep 17 00:00:00 2001 From: Amanda Lund Date: Wed, 4 Dec 2024 11:37:38 -0600 Subject: [PATCH 07/15] Add muon pair production (#1518) * Add muon pair production model and interactor * Add muon pair production test * Add muon pair production process * Fix up some things * Address review feedback * Fix narrowing warning * Fix another narrowing warning * Update data trimmer, root files, and tests * Pre-allocate capacity for clang-tidy * Address feedback --- app/celer-export-geant.cc | 1 + doc/implementation/em-physics.rst | 16 +- src/celeritas/CMakeLists.txt | 2 + src/celeritas/em/data/MuPairProductionData.hh | 125 ++++++ ...stribution.hh => MuAngularDistribution.hh} | 17 +- .../em/distribution/MuPPEnergyDistribution.hh | 254 ++++++++++++ .../em/executor/MuPairProductionExecutor.hh | 58 +++ .../interactor/MuBremsstrahlungInteractor.hh | 4 +- .../interactor/MuPairProductionInteractor.hh | 176 ++++++++ .../em/model/MuPairProductionModel.cc | 181 +++++++++ .../em/model/MuPairProductionModel.cu | 36 ++ .../em/model/MuPairProductionModel.hh | 73 ++++ .../em/process/MuPairProductionProcess.cc | 71 ++++ .../em/process/MuPairProductionProcess.hh | 70 ++++ .../ext/detail/GeantProcessImporter.cc | 26 +- src/celeritas/grid/InverseCdfFinder.hh | 4 +- src/celeritas/io/ImportDataTrimmer.cc | 47 ++- .../io/ImportMuPairProductionTable.hh | 8 +- src/celeritas/phys/ProcessBuilder.cc | 14 + src/celeritas/phys/ProcessBuilder.hh | 3 + src/corecel/data/HyperslabIndexer.hh | 21 + test/celeritas/CMakeLists.txt | 3 +- .../data/four-steel-slabs.geant.json | 3 +- test/celeritas/data/four-steel-slabs.root | Bin 77166 -> 91126 bytes test/celeritas/data/lar-sphere.root | Bin 67551 -> 67699 bytes test/celeritas/data/simple-cms.root | Bin 51780 -> 51956 bytes test/celeritas/em/MuPairProduction.test.cc | 383 ++++++++++++++++++ ....test.cc => MuAngularDistribution.test.cc} | 8 +- test/celeritas/ext/GeantImporter.test.cc | 24 +- test/celeritas/ext/RootImporter.test.cc | 2 +- test/celeritas/ext/RootJsonDumper.test.cc | 19 +- test/celeritas/phys/InteractorHostTestBase.cc | 31 +- test/celeritas/phys/InteractorHostTestBase.hh | 10 +- test/corecel/data/HyperslabIndexer.test.cc | 4 + 34 files changed, 1610 insertions(+), 84 deletions(-) create mode 100644 src/celeritas/em/data/MuPairProductionData.hh rename src/celeritas/em/distribution/{MuBremsPPAngularDistribution.hh => MuAngularDistribution.hh} (82%) create mode 100644 src/celeritas/em/distribution/MuPPEnergyDistribution.hh create mode 100644 src/celeritas/em/executor/MuPairProductionExecutor.hh create mode 100644 src/celeritas/em/interactor/MuPairProductionInteractor.hh create mode 100644 src/celeritas/em/model/MuPairProductionModel.cc create mode 100644 src/celeritas/em/model/MuPairProductionModel.cu create mode 100644 src/celeritas/em/model/MuPairProductionModel.hh create mode 100644 src/celeritas/em/process/MuPairProductionProcess.cc create mode 100644 src/celeritas/em/process/MuPairProductionProcess.hh create mode 100644 test/celeritas/em/MuPairProduction.test.cc rename test/celeritas/em/distribution/{MuBremsPPAngularDistribution.test.cc => MuAngularDistribution.test.cc} (88%) diff --git a/app/celer-export-geant.cc b/app/celer-export-geant.cc index 567af14440..a25bf78484 100644 --- a/app/celer-export-geant.cc +++ b/app/celer-export-geant.cc @@ -104,6 +104,7 @@ void run(std::string const& gdml_filename, { ImportDataTrimmer::Input options; options.mupp = true; + options.max_size = 16; ImportDataTrimmer trim(options); trim(imported); } diff --git a/doc/implementation/em-physics.rst b/doc/implementation/em-physics.rst index 4a9ad9b36a..3286b50585 100644 --- a/doc/implementation/em-physics.rst +++ b/doc/implementation/em-physics.rst @@ -61,6 +61,8 @@ The following table summarizes the EM processes and models in Celeritas. | | | Mu Bethe--Bloch | :cpp:class:`celeritas::MuHadIonizationInteractor` | 200 keV--100 TeV | | +---------------------+-----------------------------+-----------------------------------------------------+--------------------------+ | | Bremsstrahlung | Mu bremsstrahlung | :cpp:class:`celeritas::MuBremsstrahlungInteractor` | 0--100 TeV | + | +---------------------+-----------------------------+-----------------------------------------------------+--------------------------+ + | | Pair production | Mu pair production | :cpp:class:`celeritas::MuPairProductionInteractor` | 0.85 GeV--100 TeV | +----------------+---------------------+-----------------------------+-----------------------------------------------------+--------------------------+ | :math:`\mu^+` | Ionization | Bragg | :cpp:class:`celeritas::MuHadIonizationInteractor` | 0--200 keV | | + +-----------------------------+-----------------------------------------------------+--------------------------+ @@ -69,6 +71,8 @@ The following table summarizes the EM processes and models in Celeritas. | | | Mu Bethe--Bloch | :cpp:class:`celeritas::MuHadIonizationInteractor` | 200 keV--100 TeV | | +---------------------+-----------------------------+-----------------------------------------------------+--------------------------+ | | Bremsstrahlung | Mu bremsstrahlung | :cpp:class:`celeritas::MuBremsstrahlungInteractor` | 0--100 TeV | + | +---------------------+-----------------------------+-----------------------------------------------------+--------------------------+ + | | Pair production | Mu pair production | :cpp:class:`celeritas::MuPairProductionInteractor` | 0.85 GeV--100 TeV | +----------------+---------------------+-----------------------------+-----------------------------------------------------+--------------------------+ .. only:: latex @@ -120,6 +124,8 @@ The following table summarizes the EM processes and models in Celeritas. & & Mu Bethe--Bloch & \texttt{\scriptsize celeritas::MuHadIonizationInteractor} & 200 keV -- 100 TeV \\ \cline{2-5} & Bremsstrahlung & Mu bremsstrahlung & \texttt{\scriptsize celeritas::MuBremsstrahlungInteractor} & 0--100 TeV \\ + \cline{2-5} + & Pair production & Mu pair production & \texttt{\scriptsize celeritas::MuPairProductionInteractor} & 0.85 GeV--100 TeV \\ \hline \multirow{3}{*}{$\mu^+$} & \multirow{2}{*}{Ionization} & Bragg & \texttt{\scriptsize celeritas::MuHadIonizationInteractor} & 0--200 keV \\ \cline{3-5} @@ -128,6 +134,8 @@ The following table summarizes the EM processes and models in Celeritas. & & Mu Bethe--Bloch & \texttt{\scriptsize celeritas::MuHadIonizationInteractor} & 200 keV -- 100 TeV \\ \cline{2-5} & Bremsstrahlung & Mu bremsstrahlung & \texttt{\scriptsize celeritas::MuBremsstrahlungInteractor} & 0--100 TeV \\ + \cline{2-5} + & Pair production & Mu pair production & \texttt{\scriptsize celeritas::MuPairProductionInteractor} & 0.85 GeV--100 TeV \\ \bottomrule \end{tabular} \end{threeparttable} @@ -207,7 +215,7 @@ rejection sampling. Muon bremsstrahlung and pair production use a simple distribution to sample the exiting polar angles. -.. doxygenclass:: celeritas::MuBremsPPAngularDistribution +.. doxygenclass:: celeritas::MuAngularDistribution Photon scattering ----------------- @@ -221,6 +229,7 @@ Conversion/annihilation/photoelectric .. doxygenclass:: celeritas::BetheHeitlerInteractor .. doxygenclass:: celeritas::EPlusGGInteractor .. doxygenclass:: celeritas::LivermorePEInteractor +.. doxygenclass:: celeritas::MuPairProductionInteractor .. doxygenclass:: celeritas::AtomicRelaxation @@ -230,6 +239,11 @@ on the fly (as opposed to pre-tabulated cross sections). .. doxygenclass:: celeritas::EPlusGGMacroXsCalculator .. doxygenclass:: celeritas::LivermorePEMicroXsCalculator +The energy transfer for muon pair production is sampled using the inverse +transform method with tabulated CDFs. + +.. doxygenclass:: celeritas::MuPPEnergyDistribution + Coulomb scattering ------------------ diff --git a/src/celeritas/CMakeLists.txt b/src/celeritas/CMakeLists.txt index c7d653f0fb..6b8a7f9f56 100644 --- a/src/celeritas/CMakeLists.txt +++ b/src/celeritas/CMakeLists.txt @@ -30,6 +30,7 @@ list(APPEND SOURCES em/process/GammaConversionProcess.cc em/process/MuIonizationProcess.cc em/process/MuBremsstrahlungProcess.cc + em/process/MuPairProductionProcess.cc em/process/PhotoelectricProcess.cc em/process/RayleighProcess.cc ext/GeantOpticalPhysicsOptions.cc @@ -279,6 +280,7 @@ celeritas_polysource(em/model/LivermorePEModel) celeritas_polysource(em/model/MollerBhabhaModel) celeritas_polysource(em/model/MuBetheBlochModel) celeritas_polysource(em/model/MuBremsstrahlungModel) +celeritas_polysource(em/model/MuPairProductionModel) celeritas_polysource(em/model/RayleighModel) celeritas_polysource(em/model/RelativisticBremModel) celeritas_polysource(em/model/SeltzerBergerModel) diff --git a/src/celeritas/em/data/MuPairProductionData.hh b/src/celeritas/em/data/MuPairProductionData.hh new file mode 100644 index 0000000000..1c5d6511cf --- /dev/null +++ b/src/celeritas/em/data/MuPairProductionData.hh @@ -0,0 +1,125 @@ +//----------------------------------*-C++-*----------------------------------// +// Copyright 2024 UT-Battelle, LLC, and other Celeritas developers. +// See the top-level COPYRIGHT file for details. +// SPDX-License-Identifier: (Apache-2.0 OR MIT) +//---------------------------------------------------------------------------// +//! \file celeritas/em/data/MuPairProductionData.hh +//---------------------------------------------------------------------------// +#pragma once + +#include "corecel/Macros.hh" +#include "corecel/Types.hh" +#include "corecel/grid/TwodGridData.hh" +#include "celeritas/Quantities.hh" +#include "celeritas/Types.hh" + +namespace celeritas +{ +//---------------------------------------------------------------------------// +/*! + * IDs used by muon pair production. + */ +struct MuPairProductionIds +{ + ParticleId mu_minus; + ParticleId mu_plus; + ParticleId electron; + ParticleId positron; + + //! Whether the IDs are assigned + explicit CELER_FUNCTION operator bool() const + { + return mu_minus && mu_plus && electron && positron; + } +}; + +//---------------------------------------------------------------------------// +/*! + * Sampling table for electron-positron pair production by muons. + * + * The value grids are organized by atomic number, where the Z grid is a + * hardcoded, problem-independent set of atomic numbers equally spaced in \f$ + * \log Z \f$ that is linearly interpolated on. Each 2D grid is: + * - x: logarithm of the energy [MeV] of the incident charged particle + * - y: logarithm of the ratio of the energy transfer to the incident particle + * energy + * - value: CDF calculated from the differential cross section + */ +template +struct MuPairProductionTableData +{ + //// TYPES //// + + template + using Items = Collection; + + //// MEMBER DATA //// + + ItemRange logz_grid; + Items grids; + + // Backend data + Items reals; + + //// MEMBER FUNCTIONS //// + + //! Whether the data is assigned + explicit CELER_FUNCTION operator bool() const + { + return !reals.empty() && !logz_grid.empty() + && logz_grid.size() == grids.size(); + } + + //! Assign from another set of data + template + MuPairProductionTableData& + operator=(MuPairProductionTableData const& other) + { + CELER_EXPECT(other); + reals = other.reals; + logz_grid = other.logz_grid; + grids = other.grids; + return *this; + } +}; + +//---------------------------------------------------------------------------// +/*! + * Constant data for the muon pair production interactor. + */ +template +struct MuPairProductionData +{ + //// MEMBER DATA //// + + //! Particle IDs + MuPairProductionIds ids; + + //! Electron mass [MeV / c^2] + units::MevMass electron_mass; + + // Sampling table storage + MuPairProductionTableData table; + + //// MEMBER FUNCTIONS //// + + //! Whether all data are assigned and valid + explicit CELER_FUNCTION operator bool() const + { + return ids && electron_mass > zero_quantity() && table; + } + + //! Assign from another set of data + template + MuPairProductionData& operator=(MuPairProductionData const& other) + { + CELER_EXPECT(other); + ids = other.ids; + electron_mass = other.electron_mass; + table = other.table; + return *this; + } +}; + +//---------------------------------------------------------------------------// +} // namespace celeritas diff --git a/src/celeritas/em/distribution/MuBremsPPAngularDistribution.hh b/src/celeritas/em/distribution/MuAngularDistribution.hh similarity index 82% rename from src/celeritas/em/distribution/MuBremsPPAngularDistribution.hh rename to src/celeritas/em/distribution/MuAngularDistribution.hh index 68ac8f87dc..e0819830b3 100644 --- a/src/celeritas/em/distribution/MuBremsPPAngularDistribution.hh +++ b/src/celeritas/em/distribution/MuAngularDistribution.hh @@ -3,7 +3,7 @@ // See the top-level COPYRIGHT file for details. // SPDX-License-Identifier: (Apache-2.0 OR MIT) //---------------------------------------------------------------------------// -//! \file celeritas/em/distribution/MuBremsPPAngularDistribution.hh +//! \file celeritas/em/distribution/MuAngularDistribution.hh //---------------------------------------------------------------------------// #pragma once @@ -42,7 +42,7 @@ namespace celeritas * G4ModifiedMephi class and documented in section 11.2.4 of the Geant4 Physics * Reference (release 11.2). */ -class MuBremsPPAngularDistribution +class MuAngularDistribution { public: //!@{ @@ -53,9 +53,8 @@ class MuBremsPPAngularDistribution public: // Construct with incident and secondary particle quantities - inline CELER_FUNCTION MuBremsPPAngularDistribution(Energy inc_energy, - Mass inc_mass, - Energy energy); + inline CELER_FUNCTION + MuAngularDistribution(Energy inc_energy, Mass inc_mass, Energy energy); // Sample the cosine of the polar angle of the secondary template @@ -75,9 +74,9 @@ class MuBremsPPAngularDistribution * Construct with incident and secondary particle. */ CELER_FUNCTION -MuBremsPPAngularDistribution::MuBremsPPAngularDistribution(Energy inc_energy, - Mass inc_mass, - Energy energy) +MuAngularDistribution::MuAngularDistribution(Energy inc_energy, + Mass inc_mass, + Energy energy) : gamma_(1 + value_as(inc_energy) / value_as(inc_mass)) { real_type r_max_sq = ipow<2>( @@ -93,7 +92,7 @@ MuBremsPPAngularDistribution::MuBremsPPAngularDistribution(Energy inc_energy, * Sample the cosine of the polar angle of the secondary. */ template -CELER_FUNCTION real_type MuBremsPPAngularDistribution::operator()(Engine& rng) +CELER_FUNCTION real_type MuAngularDistribution::operator()(Engine& rng) { real_type a = generate_canonical(rng) * a_over_xi_; return std::cos(std::sqrt(a / (1 - a)) / gamma_); diff --git a/src/celeritas/em/distribution/MuPPEnergyDistribution.hh b/src/celeritas/em/distribution/MuPPEnergyDistribution.hh new file mode 100644 index 0000000000..d738e9a1de --- /dev/null +++ b/src/celeritas/em/distribution/MuPPEnergyDistribution.hh @@ -0,0 +1,254 @@ +//----------------------------------*-C++-*----------------------------------// +// Copyright 2024 UT-Battelle, LLC, and other Celeritas developers. +// See the top-level COPYRIGHT file for details. +// SPDX-License-Identifier: (Apache-2.0 OR MIT) +//---------------------------------------------------------------------------// +//! \file celeritas/em/distribution/MuPPEnergyDistribution.hh +//---------------------------------------------------------------------------// +#pragma once + +#include "corecel/Macros.hh" +#include "corecel/Types.hh" +#include "corecel/grid/FindInterp.hh" +#include "corecel/grid/Interpolator.hh" +#include "corecel/grid/NonuniformGrid.hh" +#include "corecel/grid/TwodGridCalculator.hh" +#include "corecel/math/Algorithms.hh" +#include "celeritas/Quantities.hh" +#include "celeritas/em/data/MuPairProductionData.hh" +#include "celeritas/grid/InverseCdfFinder.hh" +#include "celeritas/mat/ElementView.hh" +#include "celeritas/phys/CutoffView.hh" +#include "celeritas/phys/ParticleTrackView.hh" +#include "celeritas/random/distribution/UniformRealDistribution.hh" + +namespace celeritas +{ +//---------------------------------------------------------------------------// +/*! + * Sample the electron and positron energies for muon pair production. + * + * The energy transfer to the electron-positron pair is sampled using inverse + * transform sampling on a tabulated CDF. The CDF is calculated on a 2D grid, + * where the x-axis is the log of the incident muon energy and the y-axis is + * the log of the ratio of the energy transfer to the incident particle energy. + * Because the shape of the distribution depends only weakly on the atomic + * number, the CDF is calculated for a hardcoded set of points equally spaced + * in \f$ \log Z \f$ and linearly interpolated. + * + * The formula used for the differential cross section is valid when the + * maximum energy transfer to the electron-positron pair lies between \f$ + * \epsilon_{\text{min}} = 4 m \f$, where \f$ m \f$ is the electron mass, and + * \f[ + \epsilon_{\text{max}} = E + \frac{3 \sqrt{e}}{4} \mu Z^{1/3}), + * \f] + * where \f$ E = T + \mu \f$ is the total muon energy, \f$ \mu \f$ is the muon + * mass, \f$ e \f$ is Euler's number, and \f$ Z \f$ is the atomic number. + * + * The maximum energy partition between the electron and positron is calculated + * as + * \f[ + r_{\text{max}} = \left[1 - 6 \frac{\mu^2}{E (E - \epsilon)} \right] \sqrt{1 + - \epsilon_{\text{min}} / \epsilon}. + * \f] + * The partition \f$ r \f$ is then sampled uniformly in \f$ [-r_{\text{max}}, + * r_{\text{max}}) \f$. + */ +class MuPPEnergyDistribution +{ + public: + //!@{ + //! \name Type aliases + using Mass = units::MevMass; + using Energy = units::MevEnergy; + //!@} + + //! Sampled secondary energies + struct PairEnergy + { + Energy electron; + Energy positron; + }; + + public: + // Construct from shared and incident particle data + inline CELER_FUNCTION + MuPPEnergyDistribution(NativeCRef const& shared, + ParticleTrackView const& particle, + CutoffView const& cutoffs, + ElementView const& element); + + template + inline CELER_FUNCTION PairEnergy operator()(Engine& rng); + + //! Minimum energy of the electron-positron pair [MeV]. + CELER_FUNCTION Energy min_pair_energy() const + { + return Energy(min_pair_energy_); + } + + //! Maximum energy of the electron-positron pair [MeV]. + CELER_FUNCTION Energy max_pair_energy() const + { + return Energy(max_pair_energy_); + } + + private: + //// DATA //// + + // CDF table for sampling the pair energy + NativeCRef const& table_; + // Incident particle energy [MeV] + real_type inc_energy_; + // Incident particle total energy [MeV] + real_type total_energy_; + // Square of the muon mass + real_type inc_mass_sq_; + // Secondary mass + real_type electron_mass_; + // Minimum energy transfer to electron/positron pair [MeV] + real_type min_pair_energy_; + // Maximum energy transfer to electron/positron pair [MeV] + real_type max_pair_energy_; + // Minimum incident particle kinetic energy [MeV] + real_type min_energy_; + // Log Z grid interpolation for the target element + FindInterp logz_interp_; + // Coefficient for calculating the pair energy + real_type coeff_; + // Lower bound on the ratio of the pair energy to the incident energy + real_type y_min_; + // Upper bound on the ratio of the pair energy to the incident energy + real_type y_max_; + + //// HELPER FUNCTIONS //// + + // Sample the scaled energy and interpolate in log Z + template + inline CELER_FUNCTION real_type sample_scaled_energy(Engine& rng) const; + + // Calculate the scaled energy for a given Z grid and sampled CDF + inline CELER_FUNCTION real_type calc_scaled_energy(size_type z_idx, + real_type u) const; +}; + +//---------------------------------------------------------------------------// +// INLINE DEFINITIONS +//---------------------------------------------------------------------------// +/*! + * Construct from shared and incident particle data. + * + * The incident energy *must* be within the bounds of the sampling table data. + */ +CELER_FUNCTION +MuPPEnergyDistribution::MuPPEnergyDistribution( + NativeCRef const& shared, + ParticleTrackView const& particle, + CutoffView const& cutoffs, + ElementView const& element) + : table_(shared.table) + , inc_energy_(value_as(particle.energy())) + , total_energy_(value_as(particle.total_energy())) + , inc_mass_sq_(ipow<2>(value_as(particle.mass()))) + , electron_mass_(value_as(shared.electron_mass)) + , min_pair_energy_(4 * value_as(shared.electron_mass)) + , max_pair_energy_(inc_energy_ + + value_as(particle.mass()) + * (1 + - real_type(0.75) * std::sqrt(constants::euler) + * element.cbrt_z())) + , min_energy_(max(value_as(cutoffs.energy(shared.ids.positron)), + min_pair_energy_)) +{ + CELER_EXPECT(max_pair_energy_ > min_energy_); + + NonuniformGrid logz_grid(table_.logz_grid, table_.reals); + logz_interp_ = find_interp(logz_grid, element.log_z()); + + NonuniformGrid y_grid( + table_.grids[ItemId(logz_interp_.index)].y, table_.reals); + coeff_ = std::log(min_pair_energy_ / inc_energy_) / y_grid.front(); + + // Compute the bounds on the ratio of the pair energy to incident energy + y_min_ = std::log(min_energy_ / inc_energy_) / coeff_; + y_max_ = std::log(max_pair_energy_ / inc_energy_) / coeff_; + + // Check that the bounds are within the grid bounds + CELER_ASSERT(y_min_ >= y_grid.front() + || soft_equal(y_grid.front(), y_min_)); + CELER_ASSERT(y_max_ <= y_grid.back() || soft_equal(y_grid.back(), y_max_)); + y_min_ = max(y_min_, y_grid.front()); + y_max_ = min(y_max_, y_grid.back()); +} + +//---------------------------------------------------------------------------// +/*! + * Sample the exiting pair energy. + */ +template +CELER_FUNCTION auto MuPPEnergyDistribution::operator()(Engine& rng) + -> PairEnergy +{ + // Sample the energy transfer + real_type pair_energy + = inc_energy_ * std::exp(coeff_ * this->sample_scaled_energy(rng)); + CELER_ASSERT(pair_energy >= min_energy_ && pair_energy <= max_pair_energy_); + + // Sample the energy partition between the electron and positron + real_type r_max = (1 + - 6 * inc_mass_sq_ + / (total_energy_ * (total_energy_ - pair_energy))) + * std::sqrt(1 - min_pair_energy_ / pair_energy); + real_type r = UniformRealDistribution(-r_max, r_max)(rng); + + // Calculate the electron and positron energies + PairEnergy result; + real_type half_energy = pair_energy * real_type(0.5); + result.electron = Energy((1 - r) * half_energy - electron_mass_); + result.positron = Energy((1 + r) * half_energy - electron_mass_); + + CELER_ENSURE(result.electron > zero_quantity()); + CELER_ENSURE(result.positron > zero_quantity()); + return result; +} + +//---------------------------------------------------------------------------// +/*! + * Sample the scaled energy and interpolate in log Z. + */ +template +CELER_FUNCTION real_type +MuPPEnergyDistribution::sample_scaled_energy(Engine& rng) const +{ + real_type u = generate_canonical(rng); + LinearInterpolator interp_energy{ + {0, this->calc_scaled_energy(logz_interp_.index, u)}, + {1, this->calc_scaled_energy(logz_interp_.index + 1, u)}}; + return interp_energy(logz_interp_.fraction); +} + +//---------------------------------------------------------------------------// +/*! + * Calculate the scaled energy for a given Z grid and sampled CDF value. + */ +CELER_FUNCTION real_type +MuPPEnergyDistribution::calc_scaled_energy(size_type z_idx, real_type u) const +{ + CELER_EXPECT(z_idx < table_.grids.size()); + CELER_EXPECT(u >= 0 && u < 1); + + TwodGridData const& cdf_grid = table_.grids[ItemId(z_idx)]; + auto calc_cdf + = TwodGridCalculator(cdf_grid, table_.reals)(std::log(inc_energy_)); + + // Get the sampled CDF value between the y bounds + real_type cdf = LinearInterpolator{{0, calc_cdf(y_min_)}, + {1, calc_cdf(y_max_)}}(u); + + // Find the grid index of the sampled CDF value + return InverseCdfFinder(NonuniformGrid(cdf_grid.y, table_.reals), + std::move(calc_cdf))(cdf); +} + +//---------------------------------------------------------------------------// +} // namespace celeritas diff --git a/src/celeritas/em/executor/MuPairProductionExecutor.hh b/src/celeritas/em/executor/MuPairProductionExecutor.hh new file mode 100644 index 0000000000..efe928dd0b --- /dev/null +++ b/src/celeritas/em/executor/MuPairProductionExecutor.hh @@ -0,0 +1,58 @@ +//----------------------------------*-C++-*----------------------------------// +// Copyright 2024 UT-Battelle, LLC, and other Celeritas developers. +// See the top-level COPYRIGHT file for details. +// SPDX-License-Identifier: (Apache-2.0 OR MIT) +//---------------------------------------------------------------------------// +//! \file celeritas/em/executor/MuPairProductionExecutor.hh +//---------------------------------------------------------------------------// +#pragma once + +#include "corecel/Assert.hh" +#include "corecel/Macros.hh" +#include "celeritas/em/data/MuPairProductionData.hh" +#include "celeritas/em/interactor/MuPairProductionInteractor.hh" +#include "celeritas/geo/GeoTrackView.hh" +#include "celeritas/global/CoreTrackView.hh" +#include "celeritas/mat/MaterialTrackView.hh" +#include "celeritas/phys/Interaction.hh" +#include "celeritas/phys/PhysicsStepView.hh" +#include "celeritas/random/RngEngine.hh" + +namespace celeritas +{ +//---------------------------------------------------------------------------// +struct MuPairProductionExecutor +{ + inline CELER_FUNCTION Interaction + operator()(celeritas::CoreTrackView const& track); + + NativeCRef params; +}; + +//---------------------------------------------------------------------------// +/*! + * Sample muon pair production from the current track. + */ +CELER_FUNCTION Interaction +MuPairProductionExecutor::operator()(CoreTrackView const& track) +{ + auto cutoff = track.make_cutoff_view(); + auto particle = track.make_particle_view(); + auto elcomp_id = track.make_physics_step_view().element(); + CELER_ASSERT(elcomp_id); + auto element + = track.make_material_view().make_material_view().make_element_view( + elcomp_id); + auto allocate_secondaries + = track.make_physics_step_view().make_secondary_allocator(); + auto const& dir = track.make_geo_view().dir(); + + MuPairProductionInteractor interact( + params, particle, cutoff, element, dir, allocate_secondaries); + + auto rng = track.make_rng_engine(); + return interact(rng); +} + +//---------------------------------------------------------------------------// +} // namespace celeritas diff --git a/src/celeritas/em/interactor/MuBremsstrahlungInteractor.hh b/src/celeritas/em/interactor/MuBremsstrahlungInteractor.hh index 053ab575a3..b081cab047 100644 --- a/src/celeritas/em/interactor/MuBremsstrahlungInteractor.hh +++ b/src/celeritas/em/interactor/MuBremsstrahlungInteractor.hh @@ -14,7 +14,7 @@ #include "celeritas/Constants.hh" #include "celeritas/Quantities.hh" #include "celeritas/em/data/MuBremsstrahlungData.hh" -#include "celeritas/em/distribution/MuBremsPPAngularDistribution.hh" +#include "celeritas/em/distribution/MuAngularDistribution.hh" #include "celeritas/em/xs/MuBremsDiffXsCalculator.hh" #include "celeritas/mat/ElementView.hh" #include "celeritas/mat/MaterialView.hh" @@ -144,7 +144,7 @@ CELER_FUNCTION Interaction MuBremsstrahlungInteractor::operator()(Engine& rng) } while (RejectionSampler{gamma_energy * calc_dcs_(Energy{gamma_energy}), envelope_}(rng)); - MuBremsPPAngularDistribution sample_costheta( + MuAngularDistribution sample_costheta( particle_.energy(), particle_.mass(), Energy{gamma_energy}); // Update kinematics of the final state and return this interaction diff --git a/src/celeritas/em/interactor/MuPairProductionInteractor.hh b/src/celeritas/em/interactor/MuPairProductionInteractor.hh new file mode 100644 index 0000000000..469c3c62f8 --- /dev/null +++ b/src/celeritas/em/interactor/MuPairProductionInteractor.hh @@ -0,0 +1,176 @@ +//----------------------------------*-C++-*----------------------------------// +// Copyright 2024 UT-Battelle, LLC, and other Celeritas developers. +// See the top-level COPYRIGHT file for details. +// SPDX-License-Identifier: (Apache-2.0 OR MIT) +//---------------------------------------------------------------------------// +//! \file celeritas/em/interactor/MuPairProductionInteractor.hh +//---------------------------------------------------------------------------// +#pragma once + +#include "corecel/Macros.hh" +#include "corecel/Types.hh" +#include "corecel/data/StackAllocator.hh" +#include "corecel/math/Algorithms.hh" +#include "corecel/math/ArrayOperators.hh" +#include "corecel/math/ArrayUtils.hh" +#include "celeritas/Constants.hh" +#include "celeritas/Quantities.hh" +#include "celeritas/em/data/MuPairProductionData.hh" +#include "celeritas/em/distribution/MuAngularDistribution.hh" +#include "celeritas/em/distribution/MuPPEnergyDistribution.hh" +#include "celeritas/mat/ElementView.hh" +#include "celeritas/phys/CutoffView.hh" +#include "celeritas/phys/Interaction.hh" +#include "celeritas/phys/ParticleTrackView.hh" +#include "celeritas/phys/Secondary.hh" +#include "celeritas/random/distribution/UniformRealDistribution.hh" + +namespace celeritas +{ +//---------------------------------------------------------------------------// +/*! + * Perform electron-positron pair production by muons. + * + * \note This performs the same sampling routine as in Geant4's + * G4MuPairProductionModel and as documented in the Geant4 Physics Reference + * Manual (Release 11.1) section 11.3. + */ +class MuPairProductionInteractor +{ + public: + // Construct with shared and state data + inline CELER_FUNCTION + MuPairProductionInteractor(NativeCRef const& shared, + ParticleTrackView const& particle, + CutoffView const& cutoffs, + ElementView const& element, + Real3 const& inc_direction, + StackAllocator& allocate); + + // Sample an interaction with the given RNG + template + inline CELER_FUNCTION Interaction operator()(Engine& rng); + + private: + //// TYPES //// + + using Energy = units::MevEnergy; + using Mass = units::MevMass; + using Momentum = units::MevMomentum; + using UniformRealDist = UniformRealDistribution; + + //// DATA //// + + // Shared model data + NativeCRef const& shared_; + // Allocate space for the secondary particle + StackAllocator& allocate_; + // Incident direction + Real3 const& inc_direction_; + // Incident particle energy [MeV] + Energy inc_energy_; + // Incident particle mass + Mass inc_mass_; + // Incident particle momentum [MeV / c] + real_type inc_momentum_; + // Sample the azimuthal angle + UniformRealDist sample_phi_; + // Sampler for the electron-positron pair energy + MuPPEnergyDistribution sample_energy_; + + //// HELPER FUNCTIONS //// + + // Calculate the secondary particle momentum from the sampled energy + inline CELER_FUNCTION real_type calc_momentum(Energy) const; +}; + +//---------------------------------------------------------------------------// +// INLINE DEFINITIONS +//---------------------------------------------------------------------------// +/*! + * Construct with shared and state data. + */ +CELER_FUNCTION MuPairProductionInteractor::MuPairProductionInteractor( + NativeCRef const& shared, + ParticleTrackView const& particle, + CutoffView const& cutoffs, + ElementView const& element, + Real3 const& inc_direction, + StackAllocator& allocate) + : shared_(shared) + , allocate_(allocate) + , inc_direction_(inc_direction) + , inc_energy_(particle.energy()) + , inc_mass_(particle.mass()) + , inc_momentum_(value_as(particle.momentum())) + , sample_phi_(0, 2 * constants::pi) + , sample_energy_(shared, particle, cutoffs, element) +{ + CELER_EXPECT(particle.particle_id() == shared.ids.mu_minus + || particle.particle_id() == shared.ids.mu_plus); +} + +//---------------------------------------------------------------------------// +/*! + * Simulate electron-posiitron pair production by muons. + */ +template +CELER_FUNCTION Interaction MuPairProductionInteractor::operator()(Engine& rng) +{ + // Allocate secondary electron and positron + Secondary* secondaries = allocate_(2); + if (secondaries == nullptr) + { + // Failed to allocate space for a secondary + return Interaction::from_failure(); + } + + // Sample the electron and positron energies + auto energy = sample_energy_(rng); + Energy pair_energy = energy.electron + energy.positron; + + // Sample the secondary directions + MuAngularDistribution sample_costheta(inc_energy_, inc_mass_, pair_energy); + real_type phi = sample_phi_(rng); + + // Create the secondary electron + Secondary& electron = secondaries[0]; + electron.particle_id = shared_.ids.electron; + electron.energy = energy.electron; + electron.direction + = rotate(from_spherical(sample_costheta(rng), phi), inc_direction_); + + // Create the secondary electron + Secondary& positron = secondaries[1]; + positron.particle_id = shared_.ids.positron; + positron.energy = energy.positron; + positron.direction + = rotate(from_spherical(sample_costheta(rng), phi + constants::pi), + inc_direction_); + + // Construct interaction for change to the incident muon + Interaction result; + result.secondaries = {secondaries, 2}; + result.energy = inc_energy_ - pair_energy; + result.direction = make_unit_vector( + inc_momentum_ * inc_direction_ + - this->calc_momentum(electron.energy) * electron.direction + - this->calc_momentum(positron.energy) * positron.direction); + + return result; +} + +//---------------------------------------------------------------------------// +/*! + * Calculate the secondary particle momentum from the sampled energy. + */ +CELER_FUNCTION real_type +MuPairProductionInteractor::calc_momentum(Energy energy) const +{ + return std::sqrt(ipow<2>(value_as(energy)) + + 2 * value_as(shared_.electron_mass) + * value_as(energy)); +} + +//---------------------------------------------------------------------------// +} // namespace celeritas diff --git a/src/celeritas/em/model/MuPairProductionModel.cc b/src/celeritas/em/model/MuPairProductionModel.cc new file mode 100644 index 0000000000..83921570db --- /dev/null +++ b/src/celeritas/em/model/MuPairProductionModel.cc @@ -0,0 +1,181 @@ +//----------------------------------*-C++-*----------------------------------// +// Copyright 2024 UT-Battelle, LLC, and other Celeritas developers. +// See the top-level COPYRIGHT file for details. +// SPDX-License-Identifier: (Apache-2.0 OR MIT) +//---------------------------------------------------------------------------// +//! \file celeritas/em/model/MuPairProductionModel.cc +//---------------------------------------------------------------------------// +#include "MuPairProductionModel.hh" + +#include +#include + +#include "corecel/Config.hh" + +#include "corecel/cont/Range.hh" +#include "corecel/data/Collection.hh" +#include "corecel/data/CollectionBuilder.hh" +#include "corecel/data/HyperslabIndexer.hh" +#include "corecel/sys/ScopedMem.hh" +#include "celeritas/em/executor/MuPairProductionExecutor.hh" +#include "celeritas/em/interactor/detail/PhysicsConstants.hh" +#include "celeritas/global/ActionLauncher.hh" +#include "celeritas/global/CoreParams.hh" +#include "celeritas/global/TrackExecutor.hh" +#include "celeritas/grid/TwodGridBuilder.hh" +#include "celeritas/io/ImportProcess.hh" +#include "celeritas/phys/InteractionApplier.hh" +#include "celeritas/phys/PDGNumber.hh" +#include "celeritas/phys/ParticleParams.hh" + +namespace celeritas +{ +//---------------------------------------------------------------------------// +/*! + * Construct from model ID and shared data. + */ +MuPairProductionModel::MuPairProductionModel( + ActionId id, + ParticleParams const& particles, + SPConstImported data, + ImportMuPairProductionTable const& imported) + : StaticConcreteAction( + id, "pair-prod-muon", "interact by e-/e+ pair production by muons") + , imported_(data, + particles, + ImportProcessClass::mu_pair_prod, + ImportModelClass::mu_pair_prod, + {pdg::mu_minus(), pdg::mu_plus()}) +{ + CELER_EXPECT(id); + + ScopedMem record_mem("MuPairProductionModel.construct"); + + HostVal host_data; + + // Save IDs + host_data.ids.mu_minus = particles.find(pdg::mu_minus()); + host_data.ids.mu_plus = particles.find(pdg::mu_plus()); + host_data.ids.electron = particles.find(pdg::electron()); + host_data.ids.positron = particles.find(pdg::positron()); + CELER_VALIDATE(host_data.ids, + << "missing particles (required for '" + << this->description() << "')"); + + // Save particle properties + host_data.electron_mass = particles.get(host_data.ids.electron).mass(); + + // Build sampling table + CELER_VALIDATE(imported, + << "sampling table (required for '" << this->description() + << "') is empty"); + this->build_table(imported, &host_data.table); + + // Move to mirrored data, copying to device + data_ = CollectionMirror{std::move(host_data)}; + + CELER_ENSURE(data_); +} + +//---------------------------------------------------------------------------// +/*! + * Particle types and energy ranges that this model applies to. + */ +auto MuPairProductionModel::applicability() const -> SetApplicability +{ + Applicability mu_minus_applic; + mu_minus_applic.particle = this->host_ref().ids.mu_minus; + mu_minus_applic.lower = zero_quantity(); + mu_minus_applic.upper = detail::high_energy_limit(); + + Applicability mu_plus_applic = mu_minus_applic; + mu_plus_applic.particle = this->host_ref().ids.mu_plus; + + return {mu_minus_applic, mu_plus_applic}; +} + +//---------------------------------------------------------------------------// +/*! + * Get the microscopic cross sections for the given particle and material. + */ +auto MuPairProductionModel::micro_xs(Applicability applic) const + -> MicroXsBuilders +{ + return imported_.micro_xs(std::move(applic)); +} + +//---------------------------------------------------------------------------// +/*! + * Interact with host data. + */ +void MuPairProductionModel::step(CoreParams const& params, + CoreStateHost& state) const +{ + auto execute = make_action_track_executor( + params.ptr(), + state.ptr(), + this->action_id(), + InteractionApplier{MuPairProductionExecutor{this->host_ref()}}); + return launch_action(*this, params, state, execute); +} + +//---------------------------------------------------------------------------// +#if !CELER_USE_DEVICE +void MuPairProductionModel::step(CoreParams const&, CoreStateDevice&) const +{ + CELER_NOT_CONFIGURED("CUDA OR HIP"); +} +#endif + +//---------------------------------------------------------------------------// +/*! + * Construct sampling table. + */ +void MuPairProductionModel::build_table( + ImportMuPairProductionTable const& imported, HostTable* table) const +{ + CELER_EXPECT(imported); + CELER_EXPECT(table); + + // Build 2D sampling table + TwodGridBuilder build_grid{&table->reals}; + CollectionBuilder grids{&table->grids}; + for (auto const& pvec : imported.physics_vectors) + { + CELER_VALIDATE(pvec, + << "invalid grid in sampling table for '" + << this->description() << "'"); + + Array dims{static_cast(pvec.x.size()), + static_cast(pvec.y.size())}; + HyperslabIndexer index(dims); + + // Normalize the CDF + std::vector cdf(pvec.value.size()); + for (size_type i : range(dims[0])) + { + double norm = 1 / pvec.value[index(i, dims[1] - 1)]; + for (size_type j : range(dims[1])) + { + cdf[index(i, j)] = pvec.value[index(i, j)] * norm; + } + } + grids.push_back( + build_grid(make_span(pvec.x), make_span(pvec.y), make_span(cdf))); + } + + // Build log Z grid + std::vector log_z; + log_z.reserve(imported.atomic_number.size()); + for (auto z : imported.atomic_number) + { + log_z.push_back(std::log(z)); + } + table->logz_grid + = make_builder(&table->reals).insert_back(log_z.begin(), log_z.end()); + + CELER_ENSURE(*table); +} + +//---------------------------------------------------------------------------// +} // namespace celeritas diff --git a/src/celeritas/em/model/MuPairProductionModel.cu b/src/celeritas/em/model/MuPairProductionModel.cu new file mode 100644 index 0000000000..9d642dcffd --- /dev/null +++ b/src/celeritas/em/model/MuPairProductionModel.cu @@ -0,0 +1,36 @@ +//---------------------------------*-CUDA-*----------------------------------// +// Copyright 2024 UT-Battelle, LLC, and other Celeritas developers. +// See the top-level COPYRIGHT file for details. +// SPDX-License-Identifier: (Apache-2.0 OR MIT) +//---------------------------------------------------------------------------// +//! \file celeritas/em/model/MuPairProductionModel.cu +//---------------------------------------------------------------------------// +#include "MuPairProductionModel.hh" + +#include "celeritas/em/executor/MuPairProductionExecutor.hh" +#include "celeritas/global/ActionLauncher.device.hh" +#include "celeritas/global/CoreParams.hh" +#include "celeritas/global/CoreState.hh" +#include "celeritas/global/TrackExecutor.hh" +#include "celeritas/phys/InteractionApplier.hh" + +namespace celeritas +{ +//---------------------------------------------------------------------------// +/*! + * Interact with device data. + */ +void MuPairProductionModel::step(CoreParams const& params, + CoreStateDevice& state) const +{ + auto execute = make_action_track_executor( + params.ptr(), + state.ptr(), + this->action_id(), + InteractionApplier{MuPairProductionExecutor{this->device_ref()}}); + static ActionLauncher const launch_kernel(*this); + launch_kernel(*this, params, state, execute); +} + +//---------------------------------------------------------------------------// +} // namespace celeritas diff --git a/src/celeritas/em/model/MuPairProductionModel.hh b/src/celeritas/em/model/MuPairProductionModel.hh new file mode 100644 index 0000000000..ae4bf45dbe --- /dev/null +++ b/src/celeritas/em/model/MuPairProductionModel.hh @@ -0,0 +1,73 @@ +//----------------------------------*-C++-*----------------------------------// +// Copyright 2024 UT-Battelle, LLC, and other Celeritas developers. +// See the top-level COPYRIGHT file for details. +// SPDX-License-Identifier: (Apache-2.0 OR MIT) +//---------------------------------------------------------------------------// +//! \file celeritas/em/model/MuPairProductionModel.hh +//---------------------------------------------------------------------------// +#pragma once + +#include +#include + +#include "corecel/data/CollectionMirror.hh" +#include "celeritas/Quantities.hh" +#include "celeritas/em/data/MuPairProductionData.hh" +#include "celeritas/io/ImportMuPairProductionTable.hh" +#include "celeritas/phys/ImportedModelAdapter.hh" +#include "celeritas/phys/ImportedProcessAdapter.hh" +#include "celeritas/phys/Model.hh" + +namespace celeritas +{ +class ParticleParams; + +//---------------------------------------------------------------------------// +/*! + * Set up and launch the muon pair production model. + */ +class MuPairProductionModel final : public Model, public StaticConcreteAction +{ + public: + //!@{ + //! \name Type aliases + using HostRef = HostCRef; + using DeviceRef = DeviceCRef; + using SPConstImported = std::shared_ptr; + //!@} + + public: + // Construct from model ID and other necessary data + MuPairProductionModel(ActionId, + ParticleParams const&, + SPConstImported, + ImportMuPairProductionTable const&); + + // Particle types and energy ranges that this model applies to + SetApplicability applicability() const final; + + // Get the microscopic cross sections for the given particle and material + MicroXsBuilders micro_xs(Applicability) const final; + + // Apply the interaction kernel on device + void step(CoreParams const&, CoreStateHost&) const final; + + // Apply the interaction kernel + void step(CoreParams const&, CoreStateDevice&) const final; + + //!@{ + //! Access model data + HostRef const& host_ref() const { return data_.host_ref(); } + DeviceRef const& device_ref() const { return data_.device_ref(); } + //!@} + + private: + CollectionMirror data_; + ImportedModelAdapter imported_; + + using HostTable = HostVal; + void build_table(ImportMuPairProductionTable const&, HostTable*) const; +}; + +//---------------------------------------------------------------------------// +} // namespace celeritas diff --git a/src/celeritas/em/process/MuPairProductionProcess.cc b/src/celeritas/em/process/MuPairProductionProcess.cc new file mode 100644 index 0000000000..98f8c781d8 --- /dev/null +++ b/src/celeritas/em/process/MuPairProductionProcess.cc @@ -0,0 +1,71 @@ +//----------------------------------*-C++-*----------------------------------// +// Copyright 2024 UT-Battelle, LLC, and other Celeritas developers. +// See the top-level COPYRIGHT file for details. +// SPDX-License-Identifier: (Apache-2.0 OR MIT) +//---------------------------------------------------------------------------// +//! \file celeritas/em/process/MuPairProductionProcess.cc +//---------------------------------------------------------------------------// +#include "MuPairProductionProcess.hh" + +#include + +#include "corecel/Assert.hh" +#include "corecel/cont/Range.hh" +#include "celeritas/em/model/MuPairProductionModel.hh" +#include "celeritas/io/ImportProcess.hh" +#include "celeritas/phys/PDGNumber.hh" + +namespace celeritas +{ +//---------------------------------------------------------------------------// +/*! + * Construct from host data. + */ +MuPairProductionProcess::MuPairProductionProcess(SPConstParticles particles, + SPConstImported process_data, + Options options, + SPConstImportTable table) + : particles_(std::move(particles)) + , imported_(process_data, + particles_, + ImportProcessClass::mu_pair_prod, + {pdg::mu_minus(), pdg::mu_plus()}) + , options_(options) + , table_(std::move(table)) +{ + CELER_EXPECT(particles_); + CELER_EXPECT(table_); +} + +//---------------------------------------------------------------------------// +/*! + * Construct the models associated with this process. + */ +auto MuPairProductionProcess::build_models(ActionIdIter start_id) const + -> VecModel +{ + return {std::make_shared( + *start_id++, *particles_, imported_.processes(), *table_)}; +} + +//---------------------------------------------------------------------------// +/*! + * Get the interaction cross sections for the given energy range. + */ +auto MuPairProductionProcess::step_limits(Applicability applic) const + -> StepLimitBuilders +{ + return imported_.step_limits(std::move(applic)); +} + +//---------------------------------------------------------------------------// +/*! + * Name of the process. + */ +std::string_view MuPairProductionProcess::label() const +{ + return "Muon electron-positron pair production"; +} + +//---------------------------------------------------------------------------// +} // namespace celeritas diff --git a/src/celeritas/em/process/MuPairProductionProcess.hh b/src/celeritas/em/process/MuPairProductionProcess.hh new file mode 100644 index 0000000000..a889b35c01 --- /dev/null +++ b/src/celeritas/em/process/MuPairProductionProcess.hh @@ -0,0 +1,70 @@ +//----------------------------------*-C++-*----------------------------------// +// Copyright 2024 UT-Battelle, LLC, and other Celeritas developers. +// See the top-level COPYRIGHT file for details. +// SPDX-License-Identifier: (Apache-2.0 OR MIT) +//---------------------------------------------------------------------------// +//! \file celeritas/em/process/MuPairProductionProcess.hh +//---------------------------------------------------------------------------// +#pragma once + +#include + +#include "celeritas/io/ImportMuPairProductionTable.hh" +#include "celeritas/phys/Applicability.hh" +#include "celeritas/phys/AtomicNumber.hh" +#include "celeritas/phys/ImportedProcessAdapter.hh" +#include "celeritas/phys/ParticleParams.hh" +#include "celeritas/phys/Process.hh" + +namespace celeritas +{ +//---------------------------------------------------------------------------// +/*! + * Electron-positron pair production process for muons. + */ +class MuPairProductionProcess : public Process +{ + public: + //!@{ + //! \name Type aliases + using SPConstParticles = std::shared_ptr; + using SPConstImported = std::shared_ptr; + using SPConstImportTable + = std::shared_ptr; + //!@} + + // Options for the pair production process + struct Options + { + bool use_integral_xs{true}; //!> Use integral method for sampling + //! discrete interaction length + }; + + public: + // Construct from pair production data + MuPairProductionProcess(SPConstParticles particles, + SPConstImported process_data, + Options options, + SPConstImportTable table); + + // Construct the models associated with this process + VecModel build_models(ActionIdIter start_id) const final; + + // Get the interaction cross sections for the given energy range + StepLimitBuilders step_limits(Applicability range) const final; + + //! Whether to use the integral method to sample interaction length + bool use_integral_xs() const final { return options_.use_integral_xs; } + + // Name of the process + std::string_view label() const final; + + private: + SPConstParticles particles_; + ImportedProcessAdapter imported_; + Options options_; + SPConstImportTable table_; +}; + +//---------------------------------------------------------------------------// +} // namespace celeritas diff --git a/src/celeritas/ext/detail/GeantProcessImporter.cc b/src/celeritas/ext/detail/GeantProcessImporter.cc index 7442bbd3a8..85ec6855ec 100644 --- a/src/celeritas/ext/detail/GeantProcessImporter.cc +++ b/src/celeritas/ext/detail/GeantProcessImporter.cc @@ -33,6 +33,7 @@ #include "corecel/Assert.hh" #include "corecel/cont/Range.hh" +#include "corecel/data/HyperslabIndexer.hh" #include "corecel/io/Logger.hh" #include "celeritas/UnitTypes.hh" #include "celeritas/io/ImportUnits.hh" @@ -428,6 +429,11 @@ import_physics_vector(G4PhysicsVector const& g4v, Array units) //---------------------------------------------------------------------------// /*! * Import a 2D physics vector. + * + * \note In Geant4 the values are stored as a vector of vectors indexed as + * [y][x]. Because the Celeritas \c TwodGridCalculator and \c + * TwodSubgridCalculator expect the y grid values to be on the inner dimension, + * the table is inverted during import so that the x and y grids are swapped. */ ImportPhysics2DVector import_physics_2dvector(G4Physics2DVector const& g4pv, Array units) @@ -437,18 +443,22 @@ ImportPhysics2DVector import_physics_2dvector(G4Physics2DVector const& g4pv, double const y_scaling = native_value_from_clhep(units[1]); double const v_scaling = native_value_from_clhep(units[2]); + Array dims{static_cast(g4pv.GetLengthY()), + static_cast(g4pv.GetLengthX())}; + HyperslabIndexer<2> index(dims); + ImportPhysics2DVector pv; - pv.x.resize(g4pv.GetLengthX()); - pv.y.resize(g4pv.GetLengthY()); - pv.value.resize(pv.x.size() * pv.y.size()); + pv.x.resize(dims[0]); + pv.y.resize(dims[1]); + pv.value.resize(dims[0] * dims[1]); - for (auto i : range(pv.x.size())) + for (auto i : range(dims[0])) { - pv.x[i] = g4pv.GetX(i) * x_scaling; - for (auto j : range(pv.y.size())) + pv.x[i] = g4pv.GetY(i) * y_scaling; + for (auto j : range(dims[1])) { - pv.y[j] = g4pv.GetY(j) * y_scaling; - pv.value[pv.y.size() * i + j] = g4pv.GetValue(i, j) * v_scaling; + pv.y[j] = g4pv.GetX(j) * x_scaling; + pv.value[index(i, j)] = g4pv.GetValue(j, i) * v_scaling; } } CELER_ENSURE(pv); diff --git a/src/celeritas/grid/InverseCdfFinder.hh b/src/celeritas/grid/InverseCdfFinder.hh index a0623edcdb..4ad3002acf 100644 --- a/src/celeritas/grid/InverseCdfFinder.hh +++ b/src/celeritas/grid/InverseCdfFinder.hh @@ -12,6 +12,7 @@ #include "corecel/cont/Range.hh" #include "corecel/grid/Interpolator.hh" #include "corecel/math/Algorithms.hh" +#include "corecel/math/SoftEqual.hh" namespace celeritas { @@ -52,7 +53,8 @@ CELER_FUNCTION InverseCdfFinder::InverseCdfFinder(G&& grid, C&& calc_cdf) , calc_cdf_(celeritas::forward(calc_cdf)) { CELER_EXPECT(grid_.size() >= 2); - CELER_EXPECT(calc_cdf_[0] == 0 && calc_cdf_[grid_.size() - 1] == 1); + CELER_EXPECT(calc_cdf_[0] == 0 + && soft_equal(calc_cdf_[grid_.size() - 1], real_type(1))); } //---------------------------------------------------------------------------// diff --git a/src/celeritas/io/ImportDataTrimmer.cc b/src/celeritas/io/ImportDataTrimmer.cc index c77c857a29..66362b6185 100644 --- a/src/celeritas/io/ImportDataTrimmer.cc +++ b/src/celeritas/io/ImportDataTrimmer.cc @@ -23,6 +23,9 @@ struct ImportDataTrimmer::GridFilterer // Whether to keep the data at this index inline bool operator()(size_type i) const; + + // Whether to trim the data + explicit operator bool() const { return stride > 0; }; }; //---------------------------------------------------------------------------// @@ -54,6 +57,10 @@ void ImportDataTrimmer::operator()(ImportData& data) (*this)(data.atomic_relaxation_data); (*this)(data.optical_materials); + + this->for_each(data.elements); + this->for_each(data.geo_materials); + this->for_each(data.phys_materials); } if (options_.physics) @@ -62,6 +69,16 @@ void ImportDataTrimmer::operator()(ImportData& data) (*this)(data.particles); (*this)(data.processes); (*this)(data.msc_models); + + this->for_each(data.processes); + this->for_each(data.msc_models); + this->for_each(data.sb_data); + this->for_each(data.livermore_pe_data); + this->for_each(data.neutron_elastic_data); + this->for_each(data.atomic_relaxation_data); + + this->for_each(data.optical_models); + this->for_each(data.optical_materials); } if (options_.mupp) @@ -69,20 +86,6 @@ void ImportDataTrimmer::operator()(ImportData& data) // Reduce the resolution of the muon pair production table (*this)(data.mu_pair_production_data); } - - this->for_each(data.elements); - this->for_each(data.geo_materials); - this->for_each(data.phys_materials); - - this->for_each(data.processes); - this->for_each(data.msc_models); - this->for_each(data.sb_data); - this->for_each(data.livermore_pe_data); - this->for_each(data.neutron_elastic_data); - this->for_each(data.atomic_relaxation_data); - - this->for_each(data.optical_models); - this->for_each(data.optical_materials); } //---------------------------------------------------------------------------// @@ -264,7 +267,7 @@ void ImportDataTrimmer::operator()(ImportPhysics2DVector& data) { for (auto j : range(y_filter.orig_size)) { - if (x_filter(i) && y_filter(j)) + if ((!x_filter || x_filter(i)) && (!y_filter || y_filter(j))) { new_value.push_back(*src); } @@ -272,6 +275,7 @@ void ImportDataTrimmer::operator()(ImportPhysics2DVector& data) } } CELER_ASSERT(src == data.value.cend()); + CELER_ASSERT(new_value.size() == data.x.size() * data.y.size()); data.value = std::move(new_value); @@ -285,7 +289,8 @@ void ImportDataTrimmer::operator()(ImportPhysics2DVector& data) template void ImportDataTrimmer::operator()(std::vector& data) { - if (options_.max_size == numeric_limits::max()) + auto filter = this->make_filterer(data.size()); + if (!filter) { // Don't trim return; @@ -294,7 +299,6 @@ void ImportDataTrimmer::operator()(std::vector& data) std::vector result; result.reserve(std::min(options_.max_size + 1, data.size())); - auto filter = this->make_filterer(data.size()); for (auto i : range(data.size())) { if (filter(i)) @@ -312,8 +316,14 @@ void ImportDataTrimmer::operator()(std::vector& data) template void ImportDataTrimmer::operator()(std::map& data) { - std::map result; auto filter = this->make_filterer(data.size()); + if (!filter) + { + // Don't trim + return; + } + + std::map result; auto iter = data.begin(); for (auto i : range(filter.orig_size)) { @@ -369,6 +379,7 @@ auto ImportDataTrimmer::make_filterer(size_type vec_size) const -> GridFilterer */ bool ImportDataTrimmer::GridFilterer::operator()(size_type i) const { + CELER_EXPECT(stride > 0); return i % stride == 0 || i + 1 == orig_size; } diff --git a/src/celeritas/io/ImportMuPairProductionTable.hh b/src/celeritas/io/ImportMuPairProductionTable.hh index e04ebe17ac..3490edfff4 100644 --- a/src/celeritas/io/ImportMuPairProductionTable.hh +++ b/src/celeritas/io/ImportMuPairProductionTable.hh @@ -19,10 +19,10 @@ namespace celeritas * * This 3-dimensional table is used to sample the energy transfer to the * electron-positron pair, \f$ \epsilon_p \f$. The outer grid stores the atomic - * number using 5 equally spaced points in \f$ \log Z \f$; the x grid tabulates - * the ratio \f$ \log \epsilon_p / T \f$, where \f$ T \f$ is the incident muon - * energy; the y grid stores the incident muon energy using equal spacing in - * \f$ \log T \f$. + * number using 5 equally spaced points in \f$ \log Z \f$; the x grid stores + * the logarithm of the incident muon energy \f$ T \f$ using equal spacing + * in\f$ \log T \f$; the y grid stores the ratio \f$ \log \epsilon_p / T \f$. + * The values are the unnormalized CDF. */ struct ImportMuPairProductionTable { diff --git a/src/celeritas/phys/ProcessBuilder.cc b/src/celeritas/phys/ProcessBuilder.cc index ce7857a6bf..d8b1e05e77 100644 --- a/src/celeritas/phys/ProcessBuilder.cc +++ b/src/celeritas/phys/ProcessBuilder.cc @@ -21,6 +21,7 @@ #include "celeritas/em/process/GammaConversionProcess.hh" #include "celeritas/em/process/MuBremsstrahlungProcess.hh" #include "celeritas/em/process/MuIonizationProcess.hh" +#include "celeritas/em/process/MuPairProductionProcess.hh" #include "celeritas/em/process/PhotoelectricProcess.hh" #include "celeritas/em/process/RayleighProcess.hh" #include "celeritas/io/ImportData.hh" @@ -90,6 +91,8 @@ ProcessBuilder::ProcessBuilder(ImportData const& data, read_neutron_elastic_ = make_imported_element_loader(data.neutron_elastic_data); } + mu_pairprod_table_ = std::make_shared( + data.mu_pair_production_data); } //---------------------------------------------------------------------------// @@ -140,6 +143,7 @@ auto ProcessBuilder::operator()(IPC ipc) -> SPProcess {IPC::e_ioni, &ProcessBuilder::build_eioni}, {IPC::mu_brems, &ProcessBuilder::build_mubrems}, {IPC::mu_ioni, &ProcessBuilder::build_muioni}, + {IPC::mu_pair_prod, &ProcessBuilder::build_mupairprod}, {IPC::neutron_elastic, &ProcessBuilder::build_neutron_elastic}, {IPC::photoelectric, &ProcessBuilder::build_photoelectric}, {IPC::rayleigh, &ProcessBuilder::build_rayleigh}, @@ -274,6 +278,16 @@ auto ProcessBuilder::build_muioni() -> SPProcess this->particle(), this->imported(), options); } +//---------------------------------------------------------------------------// +auto ProcessBuilder::build_mupairprod() -> SPProcess +{ + MuPairProductionProcess::Options options; + options.use_integral_xs = use_integral_xs_; + + return std::make_shared( + this->particle(), this->imported(), options, mu_pairprod_table_); +} + //---------------------------------------------------------------------------// /*! * Warn and return a null process. diff --git a/src/celeritas/phys/ProcessBuilder.hh b/src/celeritas/phys/ProcessBuilder.hh index 10c187ef61..6bf2318e41 100644 --- a/src/celeritas/phys/ProcessBuilder.hh +++ b/src/celeritas/phys/ProcessBuilder.hh @@ -28,6 +28,7 @@ class MaterialParams; class ParticleParams; struct ImportData; struct ImportLivermorePE; +struct ImportMuPairProductionTable; //---------------------------------------------------------------------------// //! Options used for constructing built-in Celeritas processes @@ -115,6 +116,7 @@ class ProcessBuilder std::function read_sb_; std::function read_livermore_; std::function read_neutron_elastic_; + std::shared_ptr mu_pairprod_table_; BremsModelSelection selection_; bool brem_combined_; @@ -135,6 +137,7 @@ class ProcessBuilder auto build_eioni() -> SPProcess; auto build_mubrems() -> SPProcess; auto build_muioni() -> SPProcess; + auto build_mupairprod() -> SPProcess; auto build_msc() -> SPProcess; auto build_neutron_elastic() -> SPProcess; auto build_photoelectric() -> SPProcess; diff --git a/src/corecel/data/HyperslabIndexer.hh b/src/corecel/data/HyperslabIndexer.hh index 57bf7e52ed..b4f97d5579 100644 --- a/src/corecel/data/HyperslabIndexer.hh +++ b/src/corecel/data/HyperslabIndexer.hh @@ -22,6 +22,9 @@ namespace celeritas * Indexing is in standard C iteration order, such that final dimension * "changes fastest". For example, when indexing into a 3D grid (N=3) with * coords (i=0, j=0, k=1) the resulting index will be 1. + * + * \todo + * https://github.com/celeritas-project/celeritas/pull/1518#discussion_r1856614365 */ template class HyperslabIndexer @@ -42,6 +45,10 @@ class HyperslabIndexer //! Convert N-dimensional coordinates to an index inline CELER_FUNCTION size_type operator()(Coords const& coords) const; + //! Convert N-dimensional coordinates to an index + template + inline CELER_FUNCTION size_type operator()(Args... args) const; + private: //// DATA //// @@ -115,6 +122,20 @@ CELER_FUNCTION size_type HyperslabIndexer::operator()(Coords const& coords) c return result; } +//---------------------------------------------------------------------------// +/*! + * Convert N-dimensional coordinates to an index. + */ +template +template +CELER_FUNCTION size_type HyperslabIndexer::operator()(Args... args) const +{ + static_assert(sizeof...(Args) == N, + "Number of coordinates must match number of dimensions"); + + return this->operator()(Coords{static_cast(args)...}); +} + //---------------------------------------------------------------------------// /*! * Construct from array denoting the sizes of each dimension. diff --git a/test/celeritas/CMakeLists.txt b/test/celeritas/CMakeLists.txt index af009b4047..2d25a7e7e3 100644 --- a/test/celeritas/CMakeLists.txt +++ b/test/celeritas/CMakeLists.txt @@ -130,7 +130,7 @@ celeritas_add_test(decay/MuDecay.test.cc ${_needs_double}) # Distributions celeritas_add_test(em/distribution/EnergyLossHelper.test.cc ${_fixme_single}) -celeritas_add_test(em/distribution/MuBremsPPAngularDistribution.test.cc ${_needs_double}) +celeritas_add_test(em/distribution/MuAngularDistribution.test.cc ${_needs_double}) celeritas_add_test(em/distribution/TsaiUrbanDistribution.test.cc ${_needs_double}) # Cross section calculation @@ -147,6 +147,7 @@ celeritas_add_test(em/LivermorePE.test.cc ${_needs_double}) celeritas_add_test(em/MollerBhabha.test.cc ${_needs_double}) celeritas_add_test(em/MuBetheBloch.test.cc ${_needs_double}) celeritas_add_test(em/MuBremsstrahlung.test.cc ${_needs_double}) +celeritas_add_test(em/MuPairProduction.test.cc ${_needs_double} ${_needs_root}) celeritas_add_test(em/Rayleigh.test.cc ${_needs_double}) celeritas_add_test(em/RelativisticBrem.test.cc ${_needs_double}) celeritas_add_test(em/SeltzerBerger.test.cc ${_needs_double}) diff --git a/test/celeritas/data/four-steel-slabs.geant.json b/test/celeritas/data/four-steel-slabs.geant.json index 697e6e8746..c6c5813fa1 100644 --- a/test/celeritas/data/four-steel-slabs.geant.json +++ b/test/celeritas/data/four-steel-slabs.geant.json @@ -3,7 +3,8 @@ "coulomb_scattering": true, "muon": { "ionization": true, - "bremsstrahlung": true + "bremsstrahlung": true, + "pair_production": true }, "eloss_fluctuation": true, "em_bins_per_decade": 7, diff --git a/test/celeritas/data/four-steel-slabs.root b/test/celeritas/data/four-steel-slabs.root index d2913600ccc685c86f5489d6a7a08b5a4d4318eb..327ed8ba576644930c9505b87672fa922ac17298 100644 GIT binary patch delta 78968 zcmZ^KV{jl*({9X-ZEtLEY}-yYwvCBxTN^u@WMgk^+Z)?XzP#^Ox9*>Nr|OyRbGrLE zXKK0*1{bRk4?~awPWJXLARzb3ARr(nAfUz%U-10WeZK(ug+Yj~lPL%&M=}Tq-98BD z+8Jd{eFe&8OBxy!3S8G^f)EAgmnRs=fBfRyp;>D{oc|kW@>K?bR28?hF;!x;u(vg3 zG_rBEGhs9~wJ~+FbTM)^NVwv~`v02vRsX~FUp^3!+ApfXFZ`Di1eE>%(U1SM_J0pg z0q`>rkmcb2#%uqddQh{Z=|sGw98gSPc@_i&{OHT@b&X%Twk*uL2T1TThzID|1}vM} zXa5l(T}Yn663mk78SDEK;6P+a@r)EDCm%tMkYq8XQaRwpp<&HnF^hz{<&BcXNG$x^ zEWPcVy7c^*(thl|X#4cOY_Kz@Ki%M733Hxwv&p-C@j)cUaR5ywCpQNNHro^4xX;BJ zpx=8Ub0k0a_wvR@Kww`TZQ4nG*q{CWd;pbv$cWEROFHVOn^nn+qlW9vp2Z64Z{Y7O zZH%S?6Sgv}FMDj1*re`fSrVV3AI9r5v~GEu*jTHRzN0?&yfnD-l$k&>$bAn+!0k3j zf@$_=EU6yfhdJzDTzmZn{7k+x8Y1qDJ`RhjJGES`l8&OhBACkhuskTP>pmzmS9RTc>mVKwp#M+EEweuX&`&?ww{mq*4Jpe0j{) ztg$y<gFy0gbB7%vRBH0rH}G#&}@?iq6uA^4hyiR^C>d^~YI=CXUh4(?wL zbtZ1RE8P#Cf{;A0e{`rYgU-N9Gs3`wRqP?TiE>rLOy1^wBf885Sr`g+b<#Bx+yl31 zmz(b0kBE}V03iys5;@QX?}_)A;5KW)kd<0riEu#xgSQF=8ZdYqx5vn&+sw_6dl%Jg z+VfyWO7hul*kjM)T+nm#^~5X~QY{St))1yHj@#8Ira%XBhPc}iC1Tzz67e`RI%Fe& z$fccIRJw4T=WU#e^lhcCpt;)OKm9^Q*7iV64N{F9J7ABLJ9noA-8b7&|&O(sEuuR@UMa)3?vo z4AA4Ow#ALzA~$2dp2du%9lHXZJf@D7JN?*3l9R{pSJljJ-IKtN36Z>PK_`eMT{WR? zTLNvSJi2}mSwxy&D`?B{C+7B2!8r6ccS--d%Qr?&9_E!78_c`{&slW!AB zas!ZBM(zj-W0wSR->Xxc3T}fb0x*dwfNy*Vqe6zlnx%9!EXkpys&>e+%5#HBM ziDC=$DM2bUMnC802JnmAKgucAu0oEju4E}*PA9mRd?QiZBH+o`Fov0DEc$%oVL&;_ zOM%CGoD74SE$%;Zz0DVrX@4{JV7CZw*94Yq1L1LBOol~9S3wXC71odJ?!v^g9}fx@nj%1?VJKFwQ@^Re|lN=fooVr9Z`I~|DberKx( z>zj&?6iQlU7;WWj%O6SP)vX{wVsmvFWGoNH$ZVT%WVLvJWn(wPb-uZPV~hDKh698% z2+d{q7Fx})O@D*z)XmMHV>7NzN*{j4%sh@vPR&j9HydbcepFHrY?zrRBaK;`K1(%= zd+H;xm}GPD2+@JgtUT9z7Al3Ba3^~!yx4>^f6NweoGv|U+`+V11zD*Pq*EX%N(ML@G{*Wi4fW>Om zrIwJdPeQE5zfCNO^?|N_Qe9z!H8Gs3A)Z|~xX{`5MLt#v5};;-67xH^B(K&#$_?>Ht35%kpFR(~x>%@1(q>3;bN zZ?2F}Q5VQs>Iy~!>ytM*WDbCZNWv|;E}#=1{s5jyzC^jbj?zT;kq~;v4K{f}8FGw5YdiF5O9bSijZ2MK-Qj=YSZ2P&KyN7u~ zY(G@~O>?mWy*LX|{BdvZ@9pNn7El9u6@F&B#I+COVYh=b+!+U+YX!88711{NAA0+C z-6XO1i~(--Mhw_jCtZHmg%(&>ZH-%uI_qE^Mo!mvo>QRqxEl&D3qN~abt#$G^T4lC zFBPznZ6P}rhGn=H^8B0K+`aGWF}Hmu+O@a+kZh+ZU*i%ibX<@A;LTjbpLuJ-dz?Pk zB*7G}EmZ33xe0lhCrZ%lAUUoZ-+E)9)7yIsw z|N6-FaJi)md>*`f-rLszJh5tY>mzG1Gv5^H&RdFRafymG<(_??2a`Mp#Cw}duL&8^ zc{re`IhD6)fQQA$@}l4Rs@YXC0-tr2T8<%aS*CYr9lL)__;0Iha%MlU8`eF#Q7A$| zTv@oX6e6sdnzplh>?s^&^3nqzJEdXX`2y@?Yf~oKbr0V(-StN3h>e#jhxA51D*paX zA9SZc8uNeyp24M&&UOc+R(+BS`Eo7?7k*Hi1}6u50=y1NG(M<%Fnt{5S z>||uOae`@iw&FH>8n!}NhTzezd6@~CFL|~5;bs2andKq>mGRS$<})6P5twsY?#pEd ztJR#=242M9A@!?y+NmA_3G^j?{Oo78g0@$^JDO&?fwfmU_M^#+0ku!6@lu}+!@Eph zuWQW73UR0Q{S1Zd4tXS%ktvLS4t~_xswS|w?}g@LMNM%1jErkU{v;%042eUSr1HXD zj!@9r{$ahX5;-;6;

anD!J1FRYqrTJyBK4~*X3u9!VlkagVKBYN%PRlR5$*p_dv zN#@f{%4@%Qqbg9y$)NfO9HzvH97n1dgGWmmKbM6 z1tfcRB`Z`A-l$-{6hEbU%_(ZlX6cgLTxspIn$Qg8s(4b<=Az5j)R8NxC$af>FuxW5 zeqa%GuP)PBp}G0Mj>dm=Vvy%Y>#|Fqd!?pe({S+8H>BweF3#B8Q^}-3UYc&U^Q}G$*bGbd=|{Dhh+I~^gOL! z+N8IOX%XnD@nJ3}8Je-N8ujb=lCpI+00t7H#I#X+*@NFWsyuHwxLm0lT#I*bG=`^fOcYmmPqx58nJo zgmq_(BQW<5r}#GBenKhf>AjJC=e^a|qnpF@x$iNFyZTPq6swF&V1R?wMh5cS2NT!h z-?=U#@?Q?u|4^`sUZHf+(shLd`T#J_S0aw*(F;U(Im!Lbed!SIq2`l*0?3_u)NV=L zL1bRYG}a5iRQeqQnH~JTcjh+T?BgAFWad0|B3VrR3?Pf zX2E;3a%(86It6)8cxrSz!3MHyrU!XAd5S~yjwXXQ=sh7WJFG+(5?ey@T-r^Wtnm<=n2sF0sGGtpWXbg^ z<**0ax%1Xd{6a9Yomj*jRT64p^VPJ6Lm+-55{<+WB}ch-p}1X-1qZ^nw}twfYv3}$b>CME|9}7mu#!+*Re4P2PCNmrR>8X|euR4c=YK8=pzX3jTP=L(OI7)Q847(r+?kw$r+E z=`GtfzrFBX#0;lE(Ie?WYu8JSq9co_)V+QG+p@d<{jKTh>o%bHd#IObC(fUr=HXPk zl9}s){MJr2pKcqM(^ueF9TFl@d#GED2^3aoE|AaN2P7G9EbI9KMbX1BVg5Z9CA-?X zs1b?lTcdf=tV8FKLBua_@q^UV9*X@^B*f}U&^5lWPCYjl|2gN+k+_8C9z8A%M_=LZHqTiJRVPx0iGCv(i82(BZ}s=g^@SXcKRGYO&g@PF463*TxYi&ZRS7c$ zc?olO`p%7hfW+kF{!LgNuhM3KxuW62II3B*m$?`V4-#hx{H;m2M zK!^P^OsrR-@=;-o{5&tX@}uY*Z_jZ+sOp9x_cYF=AMj%dOC8up=?^pV2xip+@* zyTP>ta>gF>%D{`v{;=#jAS=>i&%oXwK&-wEbpzyC!(jap&Knh_ya8fhpKn(}EDj;h zB^3ifK|K*>Tr5Km=nky>KHF>Dj@$jiCTe{hSnCSRxC5D2Z%qRu zg>OAPX-4@wPl&Gk(S%dj+2Rk30bVTAN%j47+YT}!ZUe%v8i-)ZkFk|7bOq!c5u7b(DHt zL<>ym8HuyIqAig_dy>h+?^-HZJ}7ZIGBXB$+>p0!;O)Bo)$(pF+E~lRo|gd& z#9N5TxEo1Rc^ICHKP3v*m|ducPDyBK&TWu~9&6|3-zmu^OURQ>%!>#of67qs3^)^* zZrl)rmg({&)1QtdnDd#u4he^QYk;Ykke*!|9( z_T-OCnmKTNr)LOG!u;jdVJsVxWRnCm-=}L1NXb59A|>+dk-OxX2}GgK20jwV^Gf~OXmUZzeo7h;Myt2V_R#fFFSSO}etwTF z706#eUEx!Sdk}i*Q)gOr%i1XII674MCzp06MvL{6w<~&4{i9)vxlstiyF3V(HfteL z7py&$dgfPICbV_Vv2}r2b)Vq8kjW=_Qqyq9K7UdQ`&Kn4#h9#Ln6oCE)nDVjR8avpdxF5qWWV&Rq5DWh!jCh9-R|X9zdbM-0w-PwGGGav6{O z=GzfmG1)0%bvJ!Xj4ENg5e1-rG?b&%NR5lCBSKB>_(`t{mxv~*K3CwGnXqy)>7KXB zOWh1j^B%r?&WL^3&ket_!0|7&u*6;6TB})bRxZVSGkMr_;;wZtV$FN2&t5Qv+h+Pt zJY=u=36PBylkj$Y*tf(BE04e(k(Lv1bk8(bR6FbkI46E$i0*p(cOjs8<(oTO4Fv|` ze*Z?&30$7WMc*ZKv1mi2^LAVulKgPgpT17nrpq{(#vb24#g0ZYu@2q9&%2jKUfyk! z+B^AJI70Mx+;^7+d_o`ZXTfp(~pM8ehBH;2C)M1=hGZp$Ps z$b^4-Da@jvd-{7x#3%sNvnz)`&S?p?EWEn+>J5|cpuFoN@jm?`@Pvd8zDe)m2!y$3 z)}ESp@JN7mEYeLI7*Z`&!>CJoZ@t?$%9f~S6P$T8%rm}TI7CV zyqTa!>wnfrrvHXeXEulxzH_^btWAhq%uiXS=3R_c|6MblnW}nD|Z@^W`{9W?(n{ize+OChhe|$93%D^kmlG=H6Ahla}!?VBcLkcz9v!NDSyyysWEG%X>`tRX z30`1gPju(nU`JJe5s|gK6uA3ap$`Al3lZ_(UP62N|^XajU< z`kGWT-I+|*N{HYIM%7StA-acnK=`)Grhw&8c% z{E|X6ojt2$jfo9ofpR4A9Yv$Evr$H zdGj;#oP#DaduofHTT>@{MoPQTfFhZCA;#$|DhIQ8KPZ&OxCm+OmUl~zT`Q_LR(VTF z!zMRv98JAjV2u9haFq_%z#hG@uv#?+aJ^{gOK(e*ki9Y=BoEgs;T0zfhyhXMCyL~~ z>zv~o1bZs;8torL72sYaI`($L{D^$sosOkFS%jk)i}^RL8ld~%RS~$-VuU=CZ?Z9K z8AUwp?7KC{BLqEJHAdYf&hvc;A~kI1pcMT!qC_&l>=Xmy>kDhENmb(oYbI*^u)XA! zPIxnCAo$!HjO5{R!*Zwa34jqkF{sYzueHS9BKlq7#bnZ@waT>2U<>7xg+ig0Ki-);{;gzO?cLyQ=4Fx$i3)J=yM)8hhe4#KcLmL+HXaBq2@OiGRq`^; ziYQ1qr@m*=4syhZzj&jV4)U$5=yd2V6?892#1ghMXWOF9-ZA6}qvvjz_kK--r^luu zSCus$CY|#krqrAp3`lkE?9!mq2T5J?(Mt+a@uvv?b0bRUV7Ik;m&D9lESJk}BWtQ9&vjjP28 zVG8^poCFh9jJe0>=kjv&PiUs%M%{){CJ0&RW`lg}bm9BB9Mp8yGxyVQ9;nL)r>xCV8mMjR?0!!p{I=x#={LMV5BF0%|71+#~DLE&Sf2VqC?l;C^n6!fNM zCh9M5B6xsS>l&X)M(i=^(_y@caaTa#VD&cAVVnXa`twXjRHH<~#dIjVCOZ0P)Fiy- zjBqAp09kK^VJ2k)O%I21CWRVRFN1U@#Su+!<@-#^Ie@CS!Zwq_N80uqNmK(<*u|7v zRAW%u#k5^ikmvfP^-@}Dvzsv~e>1P~dnAMlmn3e`5Z5Km`MTyhBdm5zmKS$uWH`yy;VG@(m;!rlu3D z5v>K*nsqE*Gcd9f>vzFexazHFGPI4^CFOg%^t;Qm>Hhe3@T8EloeqQ~M$m+nx!{ZE8-{J4I|Z$1=Cjya z{=3DNU#T*)&aECx|dTOkSbNz`ZkMFA}IqJQJ(giM`nz3K?^+{%+-3}hiQrI za!x>BFY_2W{o{wCgz25rlKj}~_0jp~V*GN9orL?K;Um^T)u#Zi^`D5(x7OF%`VZ?W zrQ5e9&CeX9D|h~$&wtM#pJpl!>X}?=OVAq*m{=0at_-((vO3q46Y@kuIQptQ^PogS z$Kx`~$4$|DjYW^-&spDhRrx~zJ;Mjeerh%YILw5)!I5$ruI~g+JyL13_5=pTgAvRb zW@#fxJMt-0YEa4-1CBY?!$w`7pzb!mCVDR9$zlskFqXE8V}2>+q!3MpIH0>{l8<-i z{XK7;dwM{QC3+cfQS~wB1X=ait~>on1XqpWsAo*D6olC{@5dT4DcGgzutiAQFvf;zX!ilbhEx7%ZNUEF9EUW7-C# zwW`{`W+pOLG(ImPKmXc);hx?^?G$F0)pKy~p0>u~o|cEL{nSu0?o`l?W`|>6L_t|c zjVP=CW0ksM9fflmT&D0)Rv)VfygOjBF8(;(EJ&~C;C(!1F{_&6S(|4zuGY}Njl!|^ z`L&B{RuzQFfNM1PqRtZqZdR37<<}XAbE;|O&M>YS)z3J@WQ;HeT*3}TyDE>4u>{tC z+;n6bR*7`9!w&xF>rOPRvWM&*ioPFq6h&ZNth8Alf*r)QT>g%2UF-%MSpPtANQr&Q zSpo`SO#euiYFL%RnFBkx`g{h_tI2~)XE?Z;P96|xSmoY~20N&oQ=4vB)$W?{iGBLu z6Iee;TlC4isKEaQFfV4w1JUf}*u9!6EURzKi#RwybW=2s7jFSl;C}Qfi;jq3T?~

_YObw9wzcDAJ~#*UFV=ZW0e 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%0AM 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 TGqV37YPq!)8%J@u=@bS4Q79^> delta 52096 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 diff --git a/test/celeritas/data/simple-cms.root b/test/celeritas/data/simple-cms.root index aa078f7959a0fe661a08790290ace137f27aab8d..5bfa4d4f992e3582f5c2f7e71f8441413cf0c371 100644 GIT binary patch delta 38478 zcmZ^~1ymec6D>SA1b26r;O_1k+}%C64o)Cgm;iy`4#6e31PB`3-GXay=W#ED@Bi<5 zuUXCNwQHZMI#%5?v%Bjv#90D_(0fNmR{-Gr9RL6@0|5HZo=(4~YmcWB?CETTeEMe& z0Hl2Z02np^U<+dHDV4>jM-3=65{oe@6*c5@Z5v{Y*6a+ZwhFG!N_g3>y3e;lx0OaFNg8 z>(Ie1R$s%Jfjy(ILl3V4vWM4v2KD~7hpz`sz`Hzy`hP(LK9DAY!!v053O0Zy5qzJ) z*UlqKfQ%92pFyL)OvEh^1XAoXsPz{_3Ik;$X+48huizo*4f2amyT2f^Hz*O=`ppH#$ZrC=0{<88mqXZ$Ma>UeDlbo|wU)49tpW z@OA#Mp5`BG_!(6F+Z>w=B#5p048BeP_ER8ii)Zk4O<>=GuyI~`^vcAsd1B&&vT$BH zp!3%+&L#*P*Y-btagG0Ff5BCHHoT^WI}f_VwR;9%M+;9AhUCY=YP)_RBVU?F1n#Xps;fBt#gF zJ^s%c;qv^qZ2iyvsniIt!n9H*cvwqHUv3l&FrH${^ z%a>1}r6`hD`J~;8f=Bb&>>tzk5zM>;Aq-3Jxmqjv^cFZb2dt?z=n z+r5BIN!i|`N?=rcI)bfKcbEZHNm5hcX2nnZVeIrmulc+c18n<=1HY}5aAmL?hTA2- zPrsw{E2m}(x5KUfko9$-(C{F(#pAsVEYdYkTDn$Qc}Hox=);B@!a1iBAaBsU^wq$j zBKGobtJ8K`*zZgM=;~_3NB}P&e9!w8EsASywY`ycyv|P)J=K2a*I=1;z_ih7{B*KE zEKR>}MJy>whj9++#a5lB9p+v7^zz>M<2l}y4_ABaUOHm);MZW{i=0y}8LiIChLi9# z9`UfFFk3Vkot-PBdy(7z5)FRq$YsZ~O}unh7YC=`;e6?5K=(#q3>nt4e;99x=sVcu zz$Bfh)r@WrS@hZ28H+#qRWQ`85>%^AaG@97lMGj}QdNqyZSCtmiU%~DL}i`8_h~KJ zbL(M8h>=IbaM?dEDH92N)JOu4fQ+JpDQ-AIMt;0y5Ar34up8!o=u%~va%jB3}97-bXTOcb8 z8n%z*B(S$cB)fNd=ra{Gg=AvTNtCZhynEJ~t~{>v;mxcm8x3T9lakNDwKPP0?hGO@SBYV$H=+ z=p&rwT_t(n!R?hmZHEmYVLlp_hweHstMEpv1Jo&h?rfG5Et(Pk{5+b%wwR(;joN;6 zc?iBE2~G#k1PX%tD%o7p)ln@H-gQdqzO`ZdrM7AVNE;P`7E(7IG?ktBB%}Mc}x(=W5X~#7YTM5$SIp2yA+; zgmP$fCqnP5Ay6q+{Sb^Ry2%BEW?VEe^|I1RsYx}ts zgi?T|D76R<*@O^Jmfi~ZLF~B~a?OmwYfZkMLkBN9Dbm*Lt0|(@p~>J_j1Y1^S$=9k z9qeQumBWUJ5BoSKvhmqNSbT_Komw&!0s8#kM3Sc)7Jy2C!sU@<4+6 z$Wjs$T+c_9W~6-hJm0&8FVoA;okuuZ9{d+Rh8hwN;*S4h4AeGxZEvGws@u3$j>z5gos`x@5+N8x2rW}Tw z<}vk#oZXX~toEXxUTP$!t7YoTNW=RBEC zE_FeKBJxGEss1-=gPLEBt{o(wx2OH$?|;**!<#PQyJ~Wi9GWO6xe(44K;G9F9i*A0&<=Tv$Hf%-0C- zk_m%%$cAF%eHcQnkohC~f3jh3qm*l8o2!?P%^^p6n>R<0`=B)xQg%vrlLEI&?<&8; zB$J#3tRoXoy@?|)f=q&aAJjkeR@;(%b|o|Q64v%T5FL#we*9>?ZtVt{{tBapXCJ#x{aabyKo$~asu&?>0s_JWpG7(R+8 z-^7OrJbBeY)e8zHvwFu7$0{?_-Z3og^MXwKyybcE!Jz>Px+?o<=`zxZ3(#FRQYkvv zX1z8Ya&*{fJ6=m!4U%tL|B^t%A%6DQ`_ht1XxR6yY)`~T=a2lN+S5oAOU}en5^9@4 z1nDz`R0_x2t&KbrYrU(~bTbj&a%(#uV?DDf8&@d#+LYPLyQgUOOrgyhTkk0?aRbx4 zcbjG=7-t?}5Bf|apjtsXhrlDak0eX^Rwe@|Fg}EOLFZ(PU?J{H_J%Rxoum_@OC^k% z+9i z=ZeO@-Q;_NP^c_oczAC=T$WYr@zJDFHL7su$C&C)65ptl#voNjvI+1klqbB+U=^8N ze@mm#G(Ex}ns7no_#F<7P-Sc3*A}o}1xft$4dq~(Uo@L zA1Y#3yNgx6@)l}p(x2_AcCcW&_keq#GxrM_nhlEr_U0Rgq6?1h8=xbJ=4#S4C4TU9 zlEIy+(wsEq`0C$s%Mt+#ZCUlTC1MTt7V(zTquzK>z_Pxj$u2IeVMDud*$#FbF90oX zs%Vz<_LrR^j~pxT^?h678Zo;^7D&-Xtf5de-w;|x{Jng3xWrEk6_pHPnK+eg&&o2h zKk5Mk57#*wi)RbfAz-cnz(;Zdth|&N(XW7zbwJjdJN+@GbxaLuT_jOT%xcZHvw-5a6iT01ZFHSAj z;|Q?zLGtP_K*q;Tcs4vC=041pl2ELf<`igbWboO#3^>JA#&(i{W)H3KH#5@17q!W2 z0Ycm;*x3n)@5ro$T9;(Oe>Q15?pF5*EBx*+nV^;&!Ai%=m`DZ_YbwcVl@zG3S&?n% zam7q^Cx(vsL3&IkFJqe|7z&8h?Woh|d0Y*I8(cwuU9N93fbc^CF=V|( znpyCeu9|iQJc7xpGInajmHet!PT(w)K8+A_p?jtt>K>eRGFyUC z4p-6d-%zJkik7-v)nVwn2gX#nj%81thvFnSo-&}hTV5HsU;gbwrhvD!lZI}Lu>1gZ zUWPeI0FbLwr-B*=>@m*RY4D0op5N(|w_khfwp8!Oi9u6^?S&_*ei(=Gf+?yLJcUNf&}uZ(^aw( z6L-c;e+CtDATO3<*o)aq-VOQhLKl`^_r|$-v5>(b(-Wb*6Q`hmlQqIs`vK!!@ZM;Z zXf9Bm7!FI9MVxNlUu7a6g6&6*btnR+lFjI;->w)(TO$F{GLlF%l6B=L7%Y{6^}_54 zc}sC`AV0mr2Xr^+E3@Bg%y>68zp+-Q0lN)U)$b2QDQ+f)lB{*-g1vPS73mSGvs%A8 z$~9GSs0a<|)z9n0ozH{nutLSu;{R^BUB=M2U`oJi>!nkxZb z5}bD2Wt<+TM$RckenLyowB7X=YCvp>__bS+Auo)$EiYmnj%9=8Y<5lVgl!`ryNo_) z_fh%`xp{5PBh$S}YPe(KjY_yn(Gtcq-R|eQnr!cHA-n@kUEIGyP(9eGEPC(xIBRH|nT`NpBA;v*tYNo!<`OoUM*u zw=~uPvo&@ye)@jG_~1I=XU|oOxO?1~yXM3L>&6HZ?oCsf&;?kvJWS&c6uP~3 zEDvluiukk6Yoio$3B4Nb2bw*nV`ENqaFH*M#9a0Z=!p1}z}4EhGLhs>Xc?7fSWwSw z8%VwQ4z3h^ryH~cm!4oq)2-F0)vQf9g;effUcnn&d4|4&S?3rl05w{NWqtAx zkhzn#$(XUg^%m-ov4WZamv;!XbeGz~IvWs&_G`1G>9#m*z>}T0Q$qPBA)-O9 zjvALx3g*_bTZpCs&SpBB!$=@``J4Aiat?yao5=4H)cwNik^;E(P4k7m@_vYfeYauw z8|>3I`qFRPQ<9TeLGL2MK%mtQXYd7AG>K$e-065NT37S9;8NLlK&3flYv_pv%B82{ zy8Xw>vmZPEYq|)d3fh zG$Mgr;5FK8=VquA!nd5oGt7yIR(yMd>lJ*+bAiM?sP{ZkpZGl17Ok1@cO^MWKUlYc z0Y8H03+QlHpHDCH_#gp&%IFU|r_N_W5Gl611S}iSg+I_n^K!4T9@F|@4Y|~J#vq@d z@vP&_Po?k7JMu%PcKr;RXBclU>fM)Jv;rqk{fO+PYZd6SH+$cxbv95WM6=aG1(|xO zBp$BW0yPAuaF~<66(DD3aKI}CaN1G27UJ56VT^5n`XrzmNdFs%>2}yju@c#4_YFNB zT#r^o1FTJt+1>w75Cae#c2u$Xt*(~3gYP0^8VJJZHdx7 zCbz+~=Ejn!C}r67HmcIKRxU@cNPHB2p#0QO(})`k1CW)HSKrPZ1;(sBv6b z5)XME&e@TH-D6OPj@JpTl{QC;4wzr69)rfPq&_PBOcT|f&)w`}T2;A;@bbdhPJgcw`I)TP4BnZ{c)E9fYuhN!@+%{0TrEntW;el(e20%s=?Dys zN58^@%IM4f)u@}3Yo_@5QIv~bFdJ2U=%+i;s!ueY`%jKWgvc!SaKfww7>yBOXgmje1JHI9(Tf z^7r(yrjPYvg}u?B_-R_4Nm$y_<~;|Ax4Y`ptiXp6@s=^f(Vd;)kuOUr&E9^fDfnN% z3XnQM_s)dHjA`Y9Ez&EVG*JV-8}{Rl!umk^(*2s8K8F%%HX6?Qh{#zny`3GAdnp1q zEQ#U$$<4iwZ>VPAf^@dwLvAto>qn40sK5Dvh5Jj5-)(VZrR&ADY;18FzCot|%*Wrm zUzCBrsyxEuauFLnw$z5oH|Wl;=C6_ou<+HSw;DbCr67yF69;kJ@5yaU+}Ewq@uU{9 z2m^brdPcY6sJ2QQeXVX5`jJhh;m$yl+Tn=go}Vjkc0%wC1GHJbnuil68oMtJn2EL_ zgmB-edi~}R-ju0vsO%9?Ho%f!$BKnKkG}*Sn(gm6$uGsbQf3iGrT5vc-r=|3OpO%f z;wA}A3wv(2zXg+Y&52*Won3dwNO(U^x)Mer3s;(+cX#$iz<_((6hAxSO&`;~Do;m5 zam*0ayCV5xcRtSG(G4NitUj)9u zl)b=F>D#f>Zv6HiF-)?i0*K&EKQa8udxIv>o8d%ckTF!fhB!@TonfGt3|Cf=!g9ik zaT6B(R&y~OsjcCOH_w;55HS`W*R^=rQfAF|21u_A^n9t6H)=Jynu#E=Vm8DlU0m7) zlHK=rPsLF*-W#$`Q9#UgNM0=Um;`VEkGFP({9)PqWjy2~-Grx)5t)`Y5pXmoydpoM z{*ZL}P~3t%K&;n$^eX{2KyHjv_nUVQX{JCFX<6+BlY8XgwnpZh*)~Jhr@|-`xEWGD z(nrGevyShkIj6pJB=c?t_O^Y_?J(VC`;GO`c=vdUi1cikJ`aDs?u{-Q-O)O!15ZB# zX1*{iXMBKzz%JHRoz?TK&rM~Z%-ll&TNJuzOY*vjWl0`AqvCt?O5UU!J?B$aOywd1 z@xuKQsZA8$1Y6mZS@+awhY>AWTsKHJA@Rvox`VPL+W!WtGA8E^jwq%de2ZHyoU}1G zR%CB(WOuK_D;-T}_nk9llf>O=2zdV|CH4VQFMHn6yTbw#e2MV7FSGWMnCQ#Muuw9O zax2o3Ci(-?mxBb%kbyS9bdB@H<>jStZ>(TrhA)8`=EZIGX>UiXk9w$ugOA91yUbt= z57!7Kablp4fl&!PnbU`6fL-rFw>hFkJC8n_LSzOqqx~O3>zPYba2vH`O`u1@o#}q5 za6?~za_m9sfPFPYS(pU;H=TXD?}WvVbP53Nz?ze9Nq0ue=a4MbDLI*>IVana^KS@l z^L2KNxZ%338a(2(F-nr$iO=(G!M6BJ_}=hTT2c=16K(xO7HQuTT>>A#2S!J*oD6;o z&MaNc*FJ7e7Cuw-pWgc}3IHxw5toh1K;GTju-r}>LQ-0E=8Vx6hqRcwqD&D;�aR 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 delta 38299 zcmaI71yCGY8!d{vYk~)oAi;vWyAJN|?kk|S4>m7Kyc6~a*pUx(zrw=1A zu(SX$FlsR{aQ6tHWiUPZQNbS+@Fawmqc8j)S;4@r!TwPTbB_1k-eFto&(L76qYyt4;c*@^gsUo^$7-6{N%3b>HOC( z7+C7ue^LQSg1<%kS6BX@$p0MRnjlAT`Dak&8F~U+KvUp(&!FyK5W)cz4&nO@KDG7v zfP?~#K;eN{AibUidar^KP$y&*`X@dW1hx8PJ~%#AkzIu1hhHFlDDD6FfU0});VYp4 z#z%OjUPk;KjE@9-(GGsbhmUS&yS9$amhW>&xC18(8sGbh5P>;762A9`kon|2nS39A zuZ*G<@$r59u1Qawn8`;W_nF1NFB6mk&GH<>_-{JUy`WWSKXfKAKD;wef(59jSgzkK zZ>T_yFgDMsI)7DR=0RsLx6j~fnP4|T53v6&6Py@G7tZWiQ}?eqTsvqM&hZ)4{tLnj zgS6nop264J!7o=`!5e};Vf?io~m1)my*9`Ovm_7#Q-$Oc2}8GIdj7(YP^7&_13 zYq2mHL6Vpn&){ny%ydvK=1ZVgbIeB&E|%-FrpDjsSiztSETd;o{}sFgVPU&HgRgmF z2ZGYDE1tpE{=<3df1KfGP~opR?t2gyuEI0;+5xyvfpCqV!PhZ?dk?~X^U|VM%{P`$ zn)slMH!lrP|Lgb7_Sd%-|8e`);9uqVx3bTI*Ua87f-c@#K7+5t!czm;;b}gDuj%4- zffn&z($)PN3!eh?nScPKi7)l6{aQu*Oi&R4K4=_Y=2`G+LGbp;!t_7d1Ye%CUuM#4 zI|va#^Mv>yZo**P(SL>stLwi@*Z<_5PjW$^H8GegE0`|= zi^Y$r8crqFX&lZZ zk3f*{oJS%VvYh4CO`@Tb$oITdF)`y`bq~Z;M@z0FzxtR*sW)A??*t~hXd)asO$sdX zT$Q^a;AGN`5p%_d@R-6_L`H^rC*shd(;)e#cG{-Sn8HUa_IaGAiufu7<~lu=385`m zio)EUGF&d7ItN2?$>>Ct>p<3w{*aRc`qpPUY@uzWSlZJsS&E`gNhf(9fbdrIB@L{d z2O*kZ5fhq+F9rGJHaj65ebHc2kSlpxoAu?6J+y&6dyji>u05FBq_@)%oBBci_?HDI znvps^7xgEB>D&T-dqEa!;>kO8DEIugeWl6-cFDuQ^C^xfR|{|3^_K?H&hT*fjFFwR?bJ3#pT(qL)8}@R9;aBirY5;2qSm^^5&Ixdq zRgraNKPX1gCW7DIxq zbZ)e}?Rbg45JU!z5%RC^OPfDRdSNN8FQ+$E@c0}lDg+XT(F1g<)tVC%ptIPkBw;4p zNSYQoh8d4p;B@f4H&<4WNf{b%osquy{;H;RF#bdnl0W1S((UH|=Ak@;{ErOUr*Nkt zb#JgWfm@0Gj3NK)u5>vi3?M!l-DwqLUKq^DmyJv=GTxX>?b2%;Ef@?B@5>n^1izn4 zj7#iKLtA_tb%J4do_3ZH${ZM6SPMf#mKk+_ zQgXkbo#{P^R-oSwHzECYbgzhpZ*kmF>*|g&lL(6rxHk79JNuM^AwVxg?|afhKh=*h1wE zPzOXw5Lt@|0XBB^B)_u6zm*w-LukQ>uMTVbwV|du-3d^@rGKB@Dj`Moo8)h8x}V|` zTaab4X+R1nFIimam4V5cGZn?9QbxPUi|p$);8Xp@s;)kT7SL`}F^j-=Efh#V+{KK> zf;^^)`w2=9QN$T|6Lvsi`SyCnE|n!eJg>=9GjV#^t@DUr%Z2BX6XqfO%{8YDz-Fy~ zl(agu5R;c&oh7)p;5)p>?9!e7V7l2c_Pj#8h&6^BEtAK z46ZSWY<{3oAC8ph^nncT3~IJfXIvSU79(~mSHCay8yX5Q1(w($O!j?h48}gOyNozJ zJKnKUihita6NFyO=&-lgb<95;h)nqpouHwjf+Q&AWtm3Ke5r&Ja zLN#lWb6XqW$K6VKFJbomV3DuA-lk{6ne~{$tCw$GM)6jc_&Zg;@YNh4H+qYqRG0z= zEmazk7LE*^=7@~_js-7yJ=LiAf(i*VN!!5t8h;q2h3f`(GVqX5_p;lrUA8+Oz{t^; zU7MwFt%94hI&fQE`?L+_=W5RA>5Z4PexMwt*P=%N##>)sgx!7&t2;fY#_@kAadU0U z?)jbAXo)(pyooyE)WT)s*9>bm>#4!c$EvKtRR_B9iXY^=XbQB>83zLXCWL_mw9wh& z))|cj;$Sok>xp59t#I#yJfw{_2z|QqTlF%1@tm6oot4i?AY?Np_i6*~NvMi-vo%^V z4Yv+zf!G=8U~%0R+$W?uE5AOBX8y8e8EIT)E}f{h8Mo*dmf|@KRF9RR``Gv4?v0Ta zr=rKO0R*;OB4UMyo=c))dk*aGrd&8#V`~T>`7TXp4h_#{WcRLL!ol59CbZI&h?x!} z$>_x%9eWR0wW9K}aKTmEP>&dGG&_u|teLxK8<5UdHN`KKMvHpi#TI!g{1^;vA#J1u z16EX!0@_PelV69@a^=(&UNuIoqI3^}EfuGvWfvEch#(Hp4wr$oIp}*B56du)rA*?Rsp`wYO~93$Zgb4gVx`5a z_Duu5YD|_JBr#UN76+MgSeOANRN2y<5rjQmV#_RuMSCHgq=ES*7Tdk}&GVR?M>!o@ClC?gVTDu-2c>ln zTsPa(=YJ-CKu=(kH8u3?yu)iTBDb5|`j}k&m?g?w5Eb7=QCd#t2)V=B9H1fdr7eaX zTe?0IZhtpy>hgi04VcU9n=A&_>%tyBL=KmE%(X>f^M!~2k2iJk=mS`flc>MH%nq=P zlJjF9Qw?L{G!VoZQ}hu18+pL=_D-0Jw=7#^i)v1lrj01FtXmfk1mXY3dG3r z_{3$EBt&gG>LE4%{Y-?obW?69I3?Gaw7mx;fD2Q(&YhY-(ZQy2TVMu@ilmACkK^QswS{)KCYa~aTWr?nv-_h{_zI(R#zyzsx`>=R7FoC z_qtJ~MTw8D?c(0!3v~r_Mu?U>7t|foH@wzWNj`fDsj78jD2mgMZwE^Y{LC{GR+-(o zOpR>c<|Z-vtnOuW$d($kCr^ZDKN@keHC|!^Ejb{UO#CFM!-e0GWQMXhq1*!vERC4} z*q+0s0kG2NywyzeWW z5`!dOT>nJ?5<#$@WgSa}Z!&RpfVg9tZ!dLN#p!^8zKJ|5Px=}~*}%cWG-tX^%?9|+ zf(Oxc4{kDrx8KL-(=SD%l~Z0|6Ma^wphBwLr)1MFlZ|G!JM=N0w{MRKHW3`hq;7+2 zD1G9t@Ay(ExsRb?$AV3b{c~;SqEF_dYI!`66a&8vh={w5}RGuq+h&3LH;bn$ur@fKfY z8b5PTFSh$f7eq9H##mat+(#kXaHry*T?j6}yxmXaX3}x*1?;r8-#U2{Mj@5HS zp*E`xj{~Yb^>5fYxUNUafUq8wzwM&8VWuNnSFl?U=mI@GBA0b8Hs%J=A)7Q$FY0jK zhbH7W#U$`vyrH|j?7`Ey@ous-9;hl0vv0CwfYpqaXz%atcQYPr?dT@ojbnh20;)3m5ihv91znG2h5@)YpV&@kq4bomuBqjDEwnTcA7c+x(Y7XUQQD&Os$>I-4vDYLyV3<*&-^mAF7 zdZ#aj>=m8QOlo`+24tKMBh{ZB3MXYrON8&H`SiZfRRR=$4x8`r6os6O&>GT6QRK!Y3j4#LZNYp|N;fWNs6 z#$-KMO>Vi&B~NP>uP|dtvv6Jz{bC+eE6Md;`2OwZ?R-9Z7U1FXSi~_0o(oJzOx%^TTxCu}=pk zryLh|%j_>*WjTD`9qQ&6x9D5;1g|k?qQXajea(7Q|#m;lC^TzyDu?@M zq=g;nRoJg>AGN$l;MA40EK~Ft)u5e*)2AHM)BSkcNCB)n7l}O4eOuh2P_DXQB3N^@ zL(asWJ<>BQbL(_Oo26Wo(ba@*7~(o_;6;~*ynEb|yKl=5=Tr{0e4=P5xY$&Aj*w4S z=rIdyreeeJ3d1R}*Zm>NojbH^zW*kC)DBnnBdY+nZ*Q$~@yX20qjdbx)Lly6a+GiDQK%i}T(NB6CG2X@{ny!ZDn|Nv>zo{wF26kG^hu!GQh^Zhg>TcBr(^Rz&Qz!+-MingKmFJ)x>~C)L@#Q5g zGMXK#?z2gB%|&k{fwZ+YTa*!bXZt(%pQ9_!p~ry5qlOWAJwKuuXKX6Y#b-71kmIet zxvkv8NYa)5I9kx%>7l5asJw{5b{(qMVJGt>nxQ!?RK?o`m!uQjET3-l3AHIRZD1jO zGvw%L+dl&Bo}@KZtmE%9G_f~CC(D_MX<$gx$_(##6Z?~8=VPX%1NWb(T%_$yB8|9= z4%I(E@K0rv3%CUGh$&Bfw;E3KXCKanblozw?c9=3yyobXU3;0QHW|*L2t*Vj&N6i$8Fu zj(sV4vs_Ag(3+FDdCTk*Eoz)&w8La^QEctgBEPpps-^y`+Tgkdo zUE3jeBTrx0mXxlghDGgB=E)mdw0ML=hGDQ^2oSze4t#|y9rv197EIK^7Rs*vEM4$J zX2fsn*DLow69*!t+N zK8%Oo%bdj!Z1aWGq3`UwX8v>`j(HL$er%@vP_zV44s^lyOvc7aOvC#|%mDtD%J_7X z@CQHXz;--e4Q@!TCJ1QSgd)%)&=cp>!%zRYe>L-#3ofW@r=MfOA1&z@jXOULnx@jV z87~kr;F8E0o9Gh!UBa%K?EY*taHPs;IQLo)?ItV?u56rS0IO%v_%~Ds!$Pb}i4*^| z7%$|ko(&q0(WBbo9V!~n0e9gnmv7}+@0IUYXcEhdwRe-dbW%F;An7>+?Z-jxlOvPk z)+EfD0#=BHGkgd=Y(7MNz=pya&F@$JKn|gr?zLn<^Zd#ik{Z999T#qc_ zyT^vf6~5x>QK!`?wgI1f;Lg0i4W^GLy3-|J_KL_0M@~i-*Q36#JdX%Q=pK_bZveb| zLK9`icJ!zhE%fgOtRsMVXS_3)pS&2&G4pHBWaojxW|BEw1qQM?rG#G*a}&1ZvXXzN zi*?Hunm?3_EVyV*41C?IG3WH*xxlWi9*fFGKY<^ToZ&v&FZND7cap50qh;#4dXGub zgJW?1)!*01r50xLJCUs@`J8l&{&2fLFM^KpICv6XnUH&gfU^+P`2QFBcN`Ln| z_Nvf|RD>uhZesk3Cz+_(>OO>7qDcltof=~^0ZaP)x%;C1;0TWj*y8!vfG181tZ=bR zNK9?3NbgBgB*bu@oblp;oIV3=AFwC|8+R#$YRiBl`lN*)*+U;+3szXJv9p|0Ggx_) z4^_!eUX3@oqB0Gbfic8~*c)Nr?xGFWD>Jnadm@*q2Rx%_0iZg+7o({+6NZqLp*h;> zk0R-_jrvHu&VMOsKy%g%4xsEGDmIG`AS;g+oVi*A=Wf-j5^=hc{wB`Uov<53CFfpg zu%;FkO`#!saxZ*CR`@H;KedPb%_JOEdCQTtFpaqqIpYIxgs)j1S#D=%crWiBc>W0rJ$AzK4^h5761a{q}zxn4CaB+Hp<_{zkwi8CUIHU_eb7mZ7?NW zb%65uRFWeDZm{ERC%nNdlSwy^&r@KIg7Y2^Yx6?3tv&Z(zV2xV%;HdSmpI$FBcI_> ze=RZ)8vO+1A@svw*?wpqXKz(59s${4nm8m{0p3Nq; zIB7US?H)^K(!G`nTS556KU>=+Uw z?D8hiU6+2W;XM1~V@1_!*qG`N*rT=_bnB-vl0R4`ekv1?fr^I?ku!&Nf{?_t6%Npy zCuEaI5=7p%ePIQlx=7r!8=F%YhVn-{>NNO4>&o*uG)+1$O<2Hsjj~YdTWI6UIH9;R zhc@Q8e1{8R2slc2YJ6ff*qsiM+9h&QAI3W1&@6C`zAypvC<)8Lw|rHDC^ZuKjmm^u zXlSu-WH*|y$1=LD+Q88O_EoT?Jj;BEd(XQ}?pWH|i+i1Ytd!9MzRIqVBzUqfZ@bni zB;DmN8=K$3%|=Di9-I3*CV+M?zhe3zMyM@1*bz?LPNv~2oA6KX^v>hUE$ZH}+e-fi zGV#tNmU1OVpg^mH&5zHfveo6rQult_g8*OTMe&}w+9x(($V#GdemG;8RPvj5A<1xL zAOdkgOYzslL*;|7tjQPvmD>>giWV`Kw6jUPN@e#yh!PPL!pTiYI>R^#YWc&y{l=g0 z=iqdQ&`8(vUW7_44%xDnHENTD#kThzcoLLw`Cc=d-PC=|7#U)T_*T5Y?f9SPlVRQ@ z_Lx?99suSgQR05wdSG8G#8!mem-kxb$|+I7`~Ho2>9CbfH8?xmF zj$`%RLp*4@7f|S#B8qd#@ODmM)>FW?ng2x zN}&iEm1O(6@DO3j1}Qz55Y+@N(yD{Eq!@oLnviu!*T}h9*trRX_H!%}#V`1Of^#RF zCJ=_Osp`|)pOTTHd~iJ)VFJ$Aaa1&@0s7%BK(#r>g`e)t)fFBT&eblPb=FQi3ZpeI z_p$v|Cw}agr2Srqxbc$ng1Uv;1zK>J=qv}{6eLB{eK*em|IT2I>rZIv6J_t`euIXm zdtU0y{n-abHCJIkiF>pyYBlZ8=;KX&2@w+l$1znVOk@Sh>^gU$Ss!rFHSUnZyqyLS z$VC~V-mM`?r|m3MhF6L++eil3GdidtsK%}7{^+#$`^U zNZ;Fd0c-BHK{#mz2vL{h{)&pA7tuuB!yMaQXOlG~aCf5>ndlbilCe09NWA~*f?!g&)Hsj%crloPsa zaKnv)|58Sgh!&{v16@XuuvXP4rIw-9LJM(2#~8%DADa8Q=C#C5_Y)_~OrY-W2ah4% zQ$`M;B?aGsjQ8Tfl`%jiCgj|+9OLGv86%Lblfdi7|DJtAoDayo$tA0TVF-}TzG^gA z@|sXslG8C>iFThO_@vaK4v>BP-NZ}X02ro#Jct!V+f9mi+stF*JPGs5VQmSQ~YA3I2H|GFfDN=x*62Wn03OE6}E8euM#u6 z6@F5PHyUi;Ye7fRhCwJw?``QDHdJPu9r08dHs|P#zcVfVO>#x#Va)V7`DWm2I6|Z$ zph0{sekA5B4kW6L_K+v*kZn9s<}&HlW}@3rGN8D^vO5S$%Dc)CUYsi0z_8H*H1aLTbr#!Be3vX(lEn5@6X`rC)PAlIx*$D7<1>vy@P|c8F@*Dou&4 z*-2SXcdryEfCN`+6JK^eaj9W~-Si|iymwQ6w+H*iibZ3dqa?GzNqKc*N-c1xtO}t8 zcn_l|_;^|I2l*~Nn73f{@saD!MEmFYw7Xw;7|$(RJY3xjYjBkV zUU5ZZ)3P$$27K2=KNMaJ-&}7pT9EuYs-hBMy>=QGyK&?$xrU%gRNC(@ZmmtIv4`pY zNOf1vmWFI@$u@v2)F52ZyMWo*W-lY&a~+w zcfM4~%=AF^dmsmXyp!(gCT1Rk=CJ~4Ig`fxe($DZDg}eC?p?fhQ$^h;D4n$m))af zLY@va9AZWg@D<%K8GFd105%NkL36X<$f8D;`JlD@;Ia$M$<)cbiOD$r8f>#BHd)T| z_G9zLcw9vDtfLQxrw;;FElnLxNx^cr9i1A1sOoal(BN)4yOifsbkSA@Ed(x z(z3^El$b#PTXEFhw78~c8SoKxF!m7WI+-3HwfV-@omQUP^g$|pPRxl&{THJwLUVa! zJjKtU$PMbOBN3(#1K%82xh>0%ObbhWCY1*PQSr9T*OHTQ# z4ASyrvm{LUck|?!%V>@qL-*D$!b zfB9Qs(P8;%r$FrRsMX(s7x)YmQm$F7LB!~^-k2%gecaR|V`XTk>IH&**?_rxVhRsUVA$ zH=s~=8yB)x9CTaVSR_PAJU(U(2Ng_cvSKR!(YnO+r;Ic#rjnNMNf`&%ns)a2igFm* z$(f2~R}&;|D69i(U?igeFr>TqItxXOf9*@XgOC5BIt~d9%C3tu+!uM(Nov-J< zfQ6$`Eyh115{PAL{ah3UAzlK&oHb_^v)O{0mY^~yIsX)ydX7Q?1c?5z_ZzQJXgi{0 zo`t458KZVOLY$#l5yfB7MvScWYdXn-+cpgvh>i60JFHh138)|~T&{EC`D_KwY#}wC zF-$zseVe9`dfAdV(st)^!c!-$My!43a#-nuE)ULKaL)j!=^5&I!k zh`U=827(-#Ex#-h?xL>4DEU2U-;dtw(WpiAw%v9zibjWw27Bz#+MMPEGnW*iVw-8_Ko& zoU_~z7hT!sJGye{IVC~ovn5@jUurFHvd0{=uLG51B>K&r=kk3u8QxRf>f(@Add)9W zTKl*@w&)1(#u|Y7Y|mFf3NeQ|1!MVNf@l_FOShMo10x!f(|bcwxZ6e@XknvtBFFssuJahtf%m*=)1nw|ILcgi0JUrCTRedHid zO}!`lCUDWoYqhehq8|IeCg&@BaF|BmzAvt=JzWGTE6r+Ke=gU>$?~e(DD6^!OyKmF zo(P;~30Dufu-|1`tAigr2sBhtg97SvD0|S6gq0ro1)w9o1uCY9V6=_g4+Y7ppdmpL zo|up$wq*)q6nII-H<#OW;{7f?nNf#%kcD|j!3Q!J8$cLcVf<;falvUud>3{|0Bj}; z7siSP;A2FxV&uh(Cy|1X{21l#eb*GGiHU$_^N$_#t2FDs;!O}w$f1IcX zCJ?xIGdIPDAklb%f;6DLm#>9bLt@pu2%n-7hhJ(itwk^6jR-px6`Lm}&2}X( z+M|%sCM_GuZ4v_?^yW50#NP7SqyDt@2tjZp%QTC)b*lTzs!4ole=xcXW}He z$$VXIBZDG*aTE--o*fRQoSE2By~2jakA|fsK@l08Fl>bUQCLd|u~GF=@bY*T9K#HzV~SC`1J5}$wYS<)9@T|)4m9iJiV zBSWlcsD9so4^y$FW(qCl-O=)?C%tvB85Orb*)z2KRB1hk?hz@gU-$-dMW$c$Y0vz> zk|jms!9d?c0ndcGSH_^|6$n-Ag{=2Vm=jY6Es2qUJOD(X3NfN*i~k9`pM)=*zgOO> zI0HyRT=!Y?l?y4J4Qdd7p;Er`{UneL(?4d{my?|%h{{|XqTUj(m=RcUk3jr3PwpkeE2~#75mX_k z{~W^luckZ#h*aMB8MJu?V?c%SPS4mq8ikhzI{ejChzFG`ylB3Hiug|;JIGS; zrMF!EYAa@f>J(oHu&=acB}x##lJaw~*YT(n4JuF)dj|ifx%H%p55iOC{Et9c`d@*M zvcR+8b=)XF@t~DonymIW3l(aRkcz~!=4)|Ofd)Y7PpKS(R#k~XMps!zOe)t6cE%CS~s21%>EOn}#+q4rb@wHJ2ntGU|alQ}+!L7o3U>8V>h zrH2nnSAUrquZ(8(SAXp<8b5L zocl^4*1mp8$q-giK+nO#$Pv@V*3ryB&r-+A#?Z*pf!4~{7UT<0#AF7>2L=Ym2M-1| zhv^ORWcAPa*N?^jXEwYpKsr{SFrAnB`@e(%)0qJ!>yd%p=)Uxp+h2jM2?$k>7u2cy zGJ5|fIDHa$JeTCvjh@$&8{jkO_1A?y5{Obi@ELra75Z7A7X24<_rID3)F5$#m+AL9 z(+s|Yh7DfUg4adOkPake__C*^1t23_Rrr^Oaee@CLe&< z%wWm#;I7~sUhs9AN^yt93IYO~0Riu0zyu88b`VL&3elqB^kevt^pLU&7c^KYp+pE+s8Mz2HfBWu(*C_ z!q~~t_I4+7PRy#htPgdZ+Os^rp291UK}cl|u5l_dBAR)=pdGbRFjjwDu3V%VUngs= zflA_ylcTm%w8-+d!e9~$pT^A*)V}9`3Vm;z{G8-{6vYV zuu2bhn#DQ(o3f~h&64QBAjR3WtHLUdMv2-X9r$18Z!=6o%c?egPn1%!@_bGxkrS)T zCs)yEg^tKJesKQkG%{?LiRw|W*uxF!$!CAYQd`yjRN3T*yyZd{)dd^hokCL*4R<7;R zJwIy_hHVd>+qSn^l%hRcSAtIVx|iLQi<39|L1hId%#4kKgGRKBt?QBe0D!42pTAz91mXM&3HVgdv4= z4?t1*P`X0_`sY)?bYg+ta`7~h^nK6Ngq=j_Siv&JNqcMzISK|+S1Oyl_A{KIts1;^ z3ns4V?$vsxfUT*UZGe}fMjng0g>{N%`oJY>d|th;tc1BWdDP>yWQ8R?bubh4FgdC` zDP$-`t8Za0p3BDvN8s3~{KajOHw+t|hhM{30WvQq-6ZA?OcZL!hf@ZjiO>kRs6GvZ zsQpgu>~9m_B*E3 z$s-Bsk!J(K4o%>g_0UCT%5YO74ex{j!2s2<-*-$d!2 z^I94(NdSKP6GW^J3gx&Je`d2ms)%u6=?Af5Mm9-IMA%Vgu@S5gWacQ{^;NKm+RUd`1o$Y6Up3UYZR78&zSJrNm~D?iVvc0NTo7H=QeE@mYYb=EuAsvqyHkKgsh5=Cw5 zWCrE9wT^U-j-ITV9@fsisl}bEYtgW(-UNQI5B$bgTd6-^LHDDV$aumzG_958#E zOvK+)(?|YONjnxGKfHjuhfjcUz9u?sp>}KGC0cQlZxw$STX@1w#LZhII*b|vt8R#c zZp4fzT%YqB4kZUs?G%RQkK!p`c)X>MVNG$IurEN{)u*z{al!Ikbc73uj7*j%tv+yT zBN-wXdf*|oZCF-&w366X#LAM}1&th(?si@lH3b->k4*k}a;W1|>%3H^vf^RG@JhfW|-BD%6Tv5So2RTTFg>%quBwq2QR)nfKaf~Hz6_J)P$_?`H(f8KFOTy#S?rsB_ zvrK7-cO34!PKf7wEtiaB5J3F6hd4H0ua0vn>tf&a;l(a{+q6Ol1~Rc?cnlIV;v`H>PYSg;|D3A0QT zdVZQHoOS<@t<7e>yfdsl7QS?L9(T?#UNRnzQY0bE=&ORv>o+2-I|NQjWMHe@tmMW+ zoCQy_ptrNW$& z|E9~cJMt!?;0x?r@<_XoVQ1S=j@Zm#2P9L*&T&I;L$K6u))0^aMMn6HTYY|;9{8Cz z%~R1-oT>X+GgXqD>VYYe(ZmmBKLVE&f3q!DxZiVJ!#DU}Wgep7nlD=;XdtRs zp-8TI=vp>u5tp)bXh+gkQUy+3S=+&SoJzMCcmks*XqJAFG|T%}^FIv-V+A{7!B{^i z7gS5nGQF?{gF4QT<$y6Q5VAVwvXYOM5A;Cmobh?8OXtL3B#^K4TmtfG+g@`qL(9nJ z_f&_|ml8p(=Hue2E@NODCBi4NE(;;N!mnQHfrO1``GYdDMCx1Ho7t@Ce~IzWrUY_l z#Zy2u7M}wm>Y7z*2UEYN9>RWHu4=d;Rjh;L&-C1gh|Uv=VBKW zkQ+8sh#1=ge^>W87pJ)>%NTt3abwNPnYoe+Gcz+5AllriL{g@&`)q#hVJ zD|^!P&RMi0!q_UM;hYoHikR_5c~)3yAX>-9#e(~zE?f&QV7yUO^jlSRvS{EaqTn%s zvaHT=QUTrdo4_B}(7Yq&ILSwWx+lKz!Ilb4Qh?vuz$n1c6#DMqx-U+WfVINY8*psq zVSTd@yI*NyL_DY{gn~0epRM6g`H+kPU}5mXlFF)@afKW60XzxSXIjuR(5h`6Yb>7< zoCgS5&3{Td&K+eF6QH}(<8;Y9l*;4iF81rDP3THD2G099)oEKG=_RwNFDhHmO0Tn| zIDaB&0g~;Hqc4Lmn=N#t^|NhfCo}!*noCR7fI4UxzK@_Od|v=y!>xRta5!2dPh;mp zh4?vKfCMh{@^2F=0kow7l7$lf#7o^wGjgsBCZ?>TBe}EYDf>^$yd+VT8Qq?+Xay&_ zy4H|c(e@@GsK_GBD!niw2B=iOD6xiw_km5C3M&;p!a8Wze|j;t-@8MJq)$8f*nMqx z4TLe6%d;9+#w=5>8Y_&Rm@oTK`TN1w7WUR8_y}y(+KirfZB?bEI&GGIJ1d#tr$0qp+Shn&}H!7TTzSd6iF zBpZ{`+(AbnlCm=}=Da!GI`*13z&dT51xX^)FTNAuPxpkmCGkC9GK_ZgQZZR-i;Id& zu!u6{?5#DHvBYeFL8tYJX)e<;tKWU@GsIUfM0WR$xAvCajqWVy+&$O`_7thIva+3U zK<l04HmtJJgeEyfL{jR6o(&30DexSo2cLf zafyXSWS5F$W!ghRCTeY~X@*&}!*lc|aO_9g#?TmO)3zV3|8_E=`Jrysh3{Ad-Y*BKT@Se(@t<2ep$<$c z?!*xDzC3Zh;!!4il0i6MAcFmOaMR~akop1mbTgy|kuDSsW)_d{jJ43=!zByJaBNcD z6-2?-Qyo7?f#i&sB!5@4BRWHtaYfjM5At#0fjqKA=CY)Hx{F7smj>q?UE)~NOHQ>5 zJ~}I0X8s`KGv?e<0@etIsA}IlxXn%nE}kt4_WMI%QhGq=J`f7hQV7yA>9At-;0A1l z6tZGUHHSx0j$VU+@X{vJD>>~Pav8j~X%ro9{6o59X~Kl z=A+kSvP@hmVY$)df|>#5GGm0YYv-P{f;(;VW9;twEpaC2A4Kq~HUh`f`|c9yh3(P8 z8sk9Gz;7n7YB~HFI7@~unq{;wfv}KD&9d9BSk#RKAf>I;`0gG z776ok;U=NWLaV4tYIDzk-)Kw8z6-$lf1VLhoLFIn-#D^$2t~3~{G2uqr6WhJ?(C?2 zpRJ16jnWRc5dd44XnkYU?P}ZvZfB3M;egIbH;u(ct!ri{8N5N5(8pqn8pE!rt;Ioi zu1}q>bsR8;zf$(u^@r(k3*S}MHaxSBi#6N?_(BK+F)&3?;$(!m@p`BPLj@={(j4Ra zk;i(A1%^m&5QiGx)^=1&it<)%t#OnvZvAH+9OBHMm@gMT(A4+QrPvMepw;&dq?wdn zU`$M3WZsSJB2^c|%t;|vQ{<6ILRBQ;_ZR-#e@wIU(=bV=4<#bhHIzvEEcT%RU_h%l$4i%igp) zXgtfF_GRA(uz5fFAY>%#Z!AA1aiDeXp%#do2b5YMZ3%6FV^lfac?9!A)Bf8Qy-!cD zq2H4N^d+=09E%H#As3Y>W|=>Wbl5xd7#ecxg7!T==^?!?(r^ z)Qco-?;f*bm|nGt^ys#FV~U{{A-jY54w_a!j^wWE^GYxS=%a|gS@sVPDt0G zP+W2laRgO-iXZ0ZA(>6=(*E<&2ha{a9`|WxToq^75~UxjwY@UDY<`1tO`)6>TX>Tn z=%{@L5dNb!$PjZU<>91<*n@{3*LjJ6TETCAxm$=dyO@)Hg7UMMGIm&a|9P+SkHUD` zP||Fm=h0lH4M%Df((m-Q1+mZSU=L@qWl&9*J4WJxmhS@&_)1vVSU41|WT-6ZWY=%t z55+Gha*x2Sm<}n2tVqt~BKwa&0`b&(Vi}nBQAD4%#NOVZjOPbK+mTynZ}0CR98V8C za%Lcy9{jgar{4mE%eqA4F4tYH)VRiF>62C0BC5w-TSa8fRzx=X-eYkC`n+){@GJoQ zG7+g2K53|L)lgO2!Zy@CInt1m$`}d(6qh9Y_0xZujghL$Cs!9#`H@ad-x`{G6jdt+t7A>=bY8Jq6vr-|kju#f>H`#*%eV{~L; z(*+vaHYb@lnb?@vwr!go+nU&RGO=xEVohw@x95G|@BX@L-CwLoz3fJW+X9&pV%&7#O6Syd9*JQ7L=Y#ku1QxJwAX0`BzoZz9O@kODd%nlz+OhPT(axDs7B z;ZjoVkyjG@fH^V#k@Q!B?@7>W6?H=qR9llb#&!L6=tPqcj)csyW4lS`8Ywb z$Zj0GW$+V&?nmkg!}vr@nZyl<{qNkhZsYUp;a!e-0AG^ltQZ^pK&C|;o+f7TxjATI zJrw5t;0o;j#oa2xF|B-SE?g&|9~aJlM~uEQWP%b%^eu>OsSR}p{{S5hGOli_RGs@> z4Ej;-Zjve%mRXqi@*{#o_{Oqf6XY|!I6@bF z%qMkP591!^l9Zs4TAe|okKrmKDeVQkp>9u$XGM$C{Y`mw5*_BgG+GENny02WdW0I2 z4E+Z9Dh(={;OA80Vu2ky?^vFp|JHLctci4aPilE#7{)Rl=a~E+ug98QUbye|%zLc1 z!ph4_FI6G#g<435gIw6CdfEppk2y?iUyg@Ygf{@LmSL$)cEu&BC0Z+6L%a zA^Ur~z7FBIQQUD5#kS5jZ}bhkl9y#M>STi!Fg;ISluh zRnInUoZeV_|24}D(EaWH5Nm}Au0w0->|Az7x$FqLh|L5yGgsz^>De85WmXZO z%wr3>%?TGK;_jmW0D#8(rIy*NAk)C+Mxf_KXJc24nP6S7MIDfFafP`J=o3BbKCLLK zhlmY_u5ImbaCzSfAikqD=cS`r%wB^Zc!KsY9zdk)bP8N6=g0=>-$G@Ia=&)rT%a^U zG&^8v&)J(d{ZP!T6?lUlaCDn#y~0^{msG6lSG)!zgj`MlKvO5kCeKkOp9Pbb*y8S3 z$oS)M0Wqu5|FW0%J>Wjz1o#UhP|~NU7fYrR0SVQaN@v*6L${Bd8(AUB@UGVLr`{uaee6;dk)aI1^t z@!>u;b}L;@qRt&>0bA@czh}Li(t0=^rw3b|M-mNk9ltfmp6{Ciq==NCWUQxG%93}J zS9}bzD-^=B&9cAJjA6T|ywQ2)i9#tcLKP$?5>*>9>^Xtb2S0z;2xymBz`=)kBzV9a6gC>kn`hTGHO zN{mYmB-$s+6(&e!g+h3c=XA|Jl!qldt`cPP?Un~M5GGdt3n~Fwo~BE1x)X$S)v19^ zUTO+oo1fmiP-Qu|c_FTFLcRh)+|R{{$M?IF)ScqAI};FY ztQtaNoat<0Tyyb29_gpKY=C9&E8w5i<_uKvhSgMu0$iQO_H#irAZd60K%}y2{I2cH zQLgK@Gxt-01F%IxRW=>M^>KHd^gntDl*04->}4p)*I(sCo&4mV$S(tXxN~a1Zr^Um zEd5etElUv#?}e1do^6Db#r8Qq1gnEpn;CKuYDSZC7c)@EL0$;?oiW>;royU`A|I8@ zkcS%*@Mp$z4D;YCt&c_nwwR~yl{cLs>w&eAkrES84Pe=RJ&Tc|X8dbM*~`7JI~%qG zJ}d&h{_)YQx`~hnfp(zK+&I;msBY_k=MlrJC);xEvET#Ke{m^X&a7LS<%5m~c9^#& z`!nU3l*;I6!J4a#&fw!9mia}4mfH<&I?`>V;ga0vb@XbxFvj>9uQv4e=E;D&_hzrV z_lG=aH(>Kzl-9jp-FBOo`0rbx4~}ax16KDqz)EjIh&0&#)@;7X%l--BS>QMpKLQE0 z(B9g+dd`9n$A=HUcXMKE?Tlyl(so|&65{G0_PA>U4j4`~r4|}6nz;OSU?jzL=%$#b z?PCmuOm|ou!IJCmezK)cf=x4A%Kb7TtxRpE2K?5($+*-#B`UqdKR7(iJ;!mdIa>2H zKVYSV*KoJT`~6+(b#dLrYIu&JGpAB54X@jA$|u8nBsr&Ys!MWe@o;*1Zg*yR`shT*&fa}=;CMP1oxlZaU7XcqkztMP=tE<+E=);&>wdB(l;!3Oj) zGc?6ovw{llFt<{_XOFZQmva6Es9T?cq@Q607G8U$%8I=A!X8ZcgKU4xR33ldJM8i3 zioU`eID`Jr@v>b1;fKQgvrc?KAdkk&=i(&fkaZVwPF1q-FNv+>Y^Um{QP%fNTI2et zji1Knnvh5@biHT?e9fO3PRou1>?T&E^?Q#=vGDt7?=`U9QN3h&3E zPj}-Jbgp+JT6KnCTqrFSZfVOf)pt&`e;Pm)HIwGwQ1pLJb>pDQJzUZX+2`uC3Un$R z(|=Bt!K%}B1gj&EO8o0*%)5P6D4xbFL{T3yXVStbdk;V*GjMZzsGY$gB^Q z|Cn`+Hao);U<1Ewk_KH`J!Y1}UmVt)URsuKJ525+c?Z|}y$;#B0UW4ZqS6gzXe3=! zG@of?mxq0&mL$RJ|4LuwRAYW)Xu+m~Q%rM9jYEv4A}${}inAcU#&<|SrJZj=R5OEy zru}_S_HvXi^?jLlK?lX=HP-X6XSL+?Q7{JZ7jwZK@DO=o|4;S&ueMfsCvx_C;>AY$ zVU-<-S}63u8TteZguVQ$p8E(;?yBdakx516b|PB;Cm>zFy2HJ{?;G#4tyQ|Ttm8bk zH+han(l#;r>QYCftnCT?y6GkP}k#O#qoyi^+ zMobx!FfIZIZV9(DyK+LXog=rs-m$s{WpLfvT;%Mr@^{>RNvY;&Tgm~LTO6($2vxm% z-=ldoZpehzL6HIq(aE)~r)-MzzPf{1egTkDtVO(x#nKH_81qE{ks(z1Nz`Vy%XfVb z-Q?U`^r9p9oZOdlbBk|uXG>&z?-gU)N5_HNdejVO{{vIQ!rIui)sFK(i+*ZU59Aunx5f969;LQ`VciIlyz2Fhr)7q9DRy>&TY1Q z7;SntpQs|2--r=vXJ_PV%Af-qTbeT35%7KcE&6=Uvf)W>1B@C4;u2r9Gx@`N^C zJwUPSGV-(rl5Azb+p-QDds&|7%3M?DYRV|(LhJv4k-KCdcec#1r^m37?m9)*G_7RL`hEo!*6jGY{2B{WIv?RYpfV|O8*r60~Tb+O_Sr0FSpk=HJ>-vDKbqJ z@q;4a5^_w~!-p05N~eXIxYk@kZi5e_MbPdjo7-e|E$cAvS&pFcm97^Ex1=@RW@ZTJ zah}3Lz?wyMn=KkL=ijiAQ2Z4q$D8>o#Ulh^A~jS`cJfeH(*i>kX+Rh`PRoj!Ri6Lc zem>*?tkbXfu$fVr(@hx_)uUsGeX^|lw=R}pn(+7}`icc-e1idq-CI9v10C6-Q&~0j~XK9e_tTM=LTax#Rrj; zb%45cigk>4P^xN>341(VWuRBTxl*eybADpfB-qTGxO%=0FMtDKa|5Pm+@3}jx+vz= zqIwM>^g_(Hh={nE$HT9WgmCV34?3DuBRVw%VV$sKP;jj;NUdVbyuqzcT~qO$ZlJlI zMsaic9=_kI z{*u+65Mn2|CP1J*|0qKj{uy$hsT{G-2Q^T%b&DGDQvL4l)>6e}aRu(*<`gfx5(d=5s*s=Mez2b2ZYg zwmkX>G_)j&?7#^W)oA{vp*LAKnAxxR#R}vB(uFst079syUTD~F{)x5{9BYMvS&97@ zNn;NpNbsG#8?cmElameu<5Mw$rSBz)d_~(Mc^%S0mnMkq%Y#BKWMJ8^k|}B(?0V(w z%`t3e7Z>DmCHhPRjLT34&RBt>oLe+~^-~Tu7Myxg{}f>~JFfFpV3o_1-76bX!sC$s z>I^fW3co{z{>P7+pWXkXiy%2nfO%LJek4#YQ#s&BnY^xH!CAU8%G)Xr@Ct-FEqBZ# zO0($!)=X(vGwZ1K-0vB@Kn6ADKTuc+*1^nYQpCryNVHhT|4$mWLq8ppP*Iu6#&P(= zpfS9&z7qV8^0)lw$|E56fMp=%zltaXG+NnzjCQ!O@F$^>TgO_S`KNJZC{m6-lgJcv z!Bt5`OK3=4Il8s=UZj;OAZK z8h+Ayr6FHl-0cm#-|oY#Z0T9E@SDc!Gx54xEJnBq)^Uz*Qz70K@{S^(34Q+jr$Q!# zn+TgARk#K^W0kG;TGIUl3e^Vx6RH`!%7O99ZD3|9rnekuN5ttf+^h~ZPW5B1V?czX z14C?YMAyr(iKzpaz<8$EJ$$H%D6LG9ohl}2n|9znitsHO;@Izh3JOy{kLHsb8kavp15{pZEdJ1DcdRy>KC3^!qt6d)$Cft zn@ept4Vr#^d91>EUtx~xi7QNYfZu=R7B?cgWt}k~58Zp*>}-Hf{(zV_GkP>R<|sTn z6$4W7UZT{WnP$GlfoaokzN*&$vr7@aBn@G1MNQ`W0wYdQ;TlMRA`BL}IqAAob~7hp zh+ei}HoO&k9$v>v=Y6do=)UB&;qsBH4KCIvSu!|nmX5Aj4@9hTrEs4+eR z##5+b7Tk-BI2l1&rUgPUSoxkYtYye|c?>r#4q-}WKPh5{W4}?)iSwEY`^H`tn_m#G(_2ON*$>NF z+xt9_d#^~W!^6Wr^0gDi=-xHiSYi!)Hm(fTL1GMY$HSnZl9A^PSue5!E!!!#sMFN) zp3Na^%y{)<+~lrQ4P^F!7t~Cc(6y*-oIFC{0GmPXYw0v6@dhW)aB_vne{S&jY2Gvg zTW_+c^3}LQ$M|!kzzK>YO5_Z)Xu+zTB>VPUQKh{^OQDnmDxSWp4Zv13x)jRyx|L8`nCkAQQ1V`CIlUv2{B)lR)b8UPHk|=SiJf~R;si*1LkYo zP($Xs;@9xs;xY!VpFjtid*O%2AM)ge?z&&2KaJQYA&jXt5L3qZdq%!zJmuz!;cjt) z|2wu3b_VV5_7qn4Cg2g&J~L|(0Ii_O7Oi;&;lGT5Gw0i%bJC`}aLXAd=e9M=0{HDO z&7a*S))M#u);X^N%71MS<8gw^(z^W*SFQ${aIM-k|5ux4&!q#B$UM1l>qV3sa|5`# z@cR+M#4Epds||5}hA-b`I5>tlK3$d@@SGBQZOC?1B;z=XRdmn*sw1jT^-7nV681`~ zFd9o`yvuG7-Qibi-F|Gb4$1i86Wa*0Ia+bYR7w?)Hbl%ySXzor2?~h<1*%yaI=EBV z+-CgqP6b)Y-~ROGaN4FVIhC2-R6ziRSJ{j_%BU{mh7Kwo+JBgH@l zzj6x;++;iNp{*9m=Zu@jTqBA)>!l^K-~Z`FibWKA>nwq_!VqgcZgeo zHZ%CF+TgC-S}r^bEpLr8y6o{P$U+`BpV{6+S1z^B@YOE&R5Z!_OyvjuYR$~yxpfx6 z0u6gPtcjjhmDg6RUwaiG^Q~9Mi~{vjf6|0vYMlTtftQTA6v&PLhik&wEf@}2##&mm za{vv7y=XU>XGQRiS@z1q&(aeK{OA0ZN5x)b*^oM3NkE8Q=zR5_MIuwHo;^BM9H39c zFK~`s?|Lj$O2r29b%y(S;%;a2ZB1ZR3!HplubSet@X9~eKpr^KulFhyL|~8!<7#8n z4(C36ub}aE5tL#tQzDuWSl93jt3u7g2>7RB{+-{32P#z@e>OH?!lf8N5_p`)KooU| zYkZn=O-0!2w=7dHRCuncvW87Se-UL2pGTzfKb9zOG9Yh92F#ZCJLB*->3Pr&+n3(2 zT^;L}C0rd#^4)gf^ukB?kG3$Kzp8e!3))sOoaSNhTV$T?0SUKT>JH}_<@Ps#iXL{E zo?i-4RrIZ$3yem(2vv!*4%-|QQMcgW9uBT~LDWzEv>iZ@8qL&#?> zXNSF)wT$9z^E&IO?J){_Uk=G{#Y9VAIlvMjaTLWb0?-|)D2u;6(XM^kqWMGV`#X2f zb&4jHsN5T75@Nj#h>?CI@y3Q~QCbj{g_fw$d#y zq>zwHG9V2~wXEFd+momXe0_X}HtMrz<&7-|8Qi*q80?WHbHr=g6-3xB4&-dZb_|Lg z?4JF2TiY&8a-AWJ(i>yzoV~QRr_#|03&}3>jY_Pl$ z%1<3~?Cu!u2Z%zNTTVRM>r0%)=XA~O3SglcnHkKL0zF@xllhAC15?_p#-RUJ+<6%< z{7)wXwvNs3Os`JHU!^Kj`5cO38YeAR;N7ML@Up>>*gs0{R=v2#9>>CK}iKtntu z$L_$rSlBjha&vO~u#*73zrD+C|Db)zgLu)#y}d?11^lPDk|?j+14N0ZkO#Q{isb)FfpxC2K=i;sbSO1)shk4OHpww$=;y+bw)cv_@Q2l^I%Q|>cYrMmzUy@B zHo4(jO!NgUTQmOti%7!ixH@DyqN1Y@U+v+B_Eo^+P!TwfpU=;cvxVz3yG`Q*v(UEpvFg$9&1S_%4e%x^=ehqFdq7=yOCDwv4*xOdJdvQ<}aU(zdZe}SB$|| zap?ZYi*#cV5gE#6nQ1tgib8TXL`71o|1B{KIwXr#po4D1@xRmQG-kfV0e?qDypD2B z=0~=JIq%hZAC>dxs7#8{@?CeE1W18c_>mpXdX&J(Y=aWFnQfY#wye7wyml$2b*B#8 zt8&Z4#RC`5rb^ysd~sHK@DCVVLz_tzwn3ADrRgY43MhD-_aYS%4qb#{d|X+hfCsav zIHKoAP($RQlW+_B9X2P-fhHJw^O3LL88Zu2h@;oDX>z>4O*ekiPA zmeraqa}&3nd_V&>BUuSEDkzWH)bOQO|K2m=_q*|Tr3Q_Ih4LQ)h51F;6Zi_Ao5VkJ zwig#icD2YYJn&h@y0KiwCyBY-#t(%ECt<8b$_yg}IY=jNzC~)d5UGpKpo}v6IfzX1 z4d1`}iicl*ILZ7F6CRPnv4(5BBubBov9Wsng{aMwt2Y4Y0gd7+YhW{&e;`k-}+XFcBXf{DqoWJXO zP``Iuf~&S7ogevt{f#3m3ru&gCgAX7bQfq5?BZY~8oC_XBG|#6_eXl>M$CD2VEVjrd3QD}vDH?TgGsP$|ER5yc;l?y@g;cY#ylk!C*mhx zq%P-f$hPeg2)9A8+VLLybtjL z^*HqG&zaUa?1Smf6bn8-_1&ziJ%;f+N8KmIf~d>>?X&AMzqeiJzFLy@b8!#-<+kHD zeabaID8E1pLOm#CE@S=PtB%f`vX7_Mz5GCIeV-r;rHKhUnHS_F0stLd>jCsk5A-Y2Y z1FU%UyQ1aRqKX#h&ti)0qWRl5Nj^~}ts6cX!dninIW?^ZM;gKxXPWBIT9wz|9LguH z)@Ao-Hp;o2g2lf)RlJ6hRJ7heQuLalq)M~pidh7;!`{7~HRbrfhzUmeKY-0z75_Om zi1$AYTK_pW2zag!aDc}D@3}#Ez`KC$Tx|c}4PIne|2eA&0t6)E_&*;l`0wWI;G~*t zdcYSg$Z-a6pvHkOd@{kZ<7VcT?m2cYpI?%zFyj7jP@g+(4CX) z@j-t#4TlkPQ)G!g_Mo$~cXe5`mVA&#hrzygn*v?jQB>;v+I`y>-(b*Tk=*C1z8qRS za%hlu^C|C`Nzny`+n)yC;_c?dryjDkfgJz}5 zb~VY5^LC0bKLVGzJIUhgjJ~08{>-WgU#a%!5!ZP34!l|6&Y{>6X-{^9#wbAZMYa7a zNN(h=8k{!RF|gUx4o9?!>7YP$2-KddM-GTb-V9oQ(UDM=@R~D4c?$f{|64N-1CQ;o z5o=u$9*okpHKy}wvr5R*D#unlxh=ayKa`T~AxZ>(kd>P=-A2umP0RJugtkAUuY;EE zJ%(k=kkI~Di-7K}x_q%Td;9mKh7Y9zmB))!pC1nNnUFy%Mf6-+EcS1bS)#>!nV zmJUj!(vL)muRe|*&^q1L=cHDgDhy)8-)QPAv_}qonXxF4a{)bx_B~1=)WI6;KQ43l z;%7`q4o`gSz`Ij6b>?MdOm%oP`RwD)1^g?|V%?cr{Gj!ZhHn4< zScyq}cGM<&_P~B(5dsEQGh*fdKlXOw@ZA-~MLgV6=bCbhF@R7?y@*c&1|H_SW{$fW zJ}HT+jr-ERfzNrT`7bjU>fS4Ik0vXhTPe?ykOPNR(2r zH{WXseF$fbTVKRBqI2+8&@anXTm8LGSwcy_`gRm-`I|qA|leN=#iYsF>wV|pOHh?ok z(?E)pp0Y$%#Ie-!_jb069xcvWauCl@N*(8k3~$f-8@ji@ke@!Kj&_hmTH=vp4=!Uy z8_ z&+Kh_OZQGkQb*VDhRQ)AxHE^!b|s_(vCm`V_r}t}qRe#u1NGu%4?%ps( zua45UnB#N~k`WmEUno zp2OvCDzda0Fz?!0;p1SX21N9mQ05{xFPOZ=zpYX-#asi*VZQLtKOrgKgXNwxM5uEi zyF^j=e~~<;O7?h>&Wk^%qA&7j8Nb#@oSyx)M0{@DktNuRIFN`ntGlM8d*>UmZCA2i zShL}hb0P`@WJ{#1njeY55)^U3OWi+m4{<8-BN^1ez%4<%eDr2jhaZgG)XaD3SPL|$8^0F@)5>55<_fHIlU-A5WhTFk4+{v6k?9mbD1X<=g3Uk<^!{R*W z&O(Ri7v!m&J^Z)u%^I5`;QSdUut{zsWMF=HY9|0j6bN^X(ZU1T+j>y*Qr%h-dh-7^ z+j*0JAQC3p0eXs(&wmGIZK$USg?N7a<^yd^iwf@n70Fopdb-vb?1wZVyHxyLx!2#{;=UYZ_v;tKYw^~c4cEZ1 zd?RNJSfjHPy%X>nZfIBe8dU&O))nPlTQX?Yt6^WE9;xsiv*T@3htawv?B+F+5Qs zpREm;zinuX)|I7y!!%s6*ikAIB4J0%T}0xQR-y^$65l-{ln^+NharVy9xk&< zkCH1Kf=8*byeD_PTLCR9_^<@^ZJ6%u5jrPO-Za@fLw8D9HhA#SL>4G^2cQNSKf2?7 ztU(RG2!$k2PGUzVayu50@!SA(HzGJx>jNUDO7{{2*IjFm=z9wj{+8}s7JWkX_o&xG z7xa?{FwkIMKCq&jIowP~?(YrM@sVb?|y$)1&_uEoEy3!;$&9+CS-#eP@7o9{2R z-%w0~qQw(S>qQC}>n;5(WTK@9IsW`K-o{c826<%#L50?bbw-KTz#s-FzN#7dCbcz* z;?EJz!!3qAo8seZ&LRzFP+hIvc-mRfJ0pTEFWyKU;13c5j-1^C{Tl1QDI>L3K+z$G z7Gv#R&tVeE!&7pHyrndr-#}XKJh~{(ifrbTLVBmam24oO#8dnRMvI>dRM0Pyrj^7f zSlyDDns?E`DAMT61>^u0u?Y(M!1uYXRuH`3bvGrvs0O);X^BARdTWzq(?tDQrIOYl zfi{d^fZwNDT@X}OSEz`V8k8R}cL6?AsiPnIgCr!vO2^AkJ1!M9Qx3z@B5xspg|!pl za%7YhQ}V1%mZkF3hb~FnnF2BMF%Bv2y!mR=@~Zx1FUR`EbOz8A)&Esz)iZN3Wdptw?SlOb&(>@`)SrsVF8eag2O&PCP2ry>s>Jx(HFetiLVK7B#n*CC-9K>{&2Akl3+^CfTU z875^1a56hP?_OV|3$&M}gqzAbvUNQ_S}^BZo4q|29R*-l01O0C_luH_0*irUV2jGl zL~MZ5O+>v#mzkyJ^_s8WDHz8TD=btrmJrl!;*LToTS~NIn!vf3oz9>30o{y#gx2q6 z>dx_Rf4KQ36plf(E3gEoFZczmY$hmDK)K(k45SJjAiJdut7xs9;I+*>${&Z0Aef`F zG8__OFAl)TME!fo*tQy|Ei$Jxcai2JpZUsK-8+KYndOZ0pg6I=f-4T1naePRETiWw zJ3J-(%Z#YCTUO;5kh3*ee>y()no(f~1EZ|4;y*KWq{vf9z~B&%a23ESCE`RS_Cw`P zM6}|tLlefD2t2rMs{=t8t+M-Rnv!f9wO$W|Oc+1}FKv`p5Jx?ogPtn9a}l0%vwA@az^kevQ>PSBe3O3_l_Cg9hEfyQ$vaLz6QdjEr5eoji9S z4O}0k3&HA->a{eMs8}H$TA}e9eEEq-gU+~jPR}_=&w3r|<4urz;G%udEaXBo{Jv9V zgqR@35Lrt|p->dUx3ufVmn&^dE{M+_LJ7cZro2}pY<14yo7GaG*$=k8OODT>?Dbh( zg>N_Fk?z|O4YDyC(Bvh}B66hdYpw|a?|Q}L--AQ!!!*Sat{S4q|L`R_;aD!uzZ;4= z9j{O3YgrMdn%QnJ49*H0L7!4uwd!nWS(a!E@aHJF8S9GOPoj z{FQbX=~$08m>)7Hex$;m>xk#P9lk-ge2Qd1O7(Mt`(C-iJ`QdIb(i5%TiEV(qFX!K zd|op>(iWBZ+0Hk=|HI0+B*iZU(%9&|_VWpVvQe8_G1nv)_QvLbkcl+1Q*Ug{B(h4q zEUhg1g3;h(M}&zy;4m}PBH`X9$Glkcr71eP{0!iMI#~+5-c1Sxu-4L3pp?C+_KjW$_M{jK@?omjMs&aPA zC&Dhdxidw|j&Pp&q_*O1B$b5OnADY5byZ7ZdKSm8-1xy6s0=C?gdIlTVaWl(ADJK^ zv?jjfbk&=!f6+Aa$)%{o{(UJl{7~L>84@RI9@MvHtVC-Qg6PCTIk*8jGYQKgu3@rp zim`MN$F)s%&!Q~qVt9=g66?7nl}ED)V%zDQ7M2ZvddZ<8NgyWEF*KpOJGqBH*^mOi znp7!Rq0A^r>dcW@bCy1#12lZyx5!?jS73&WNp7h}{w?wH#F{z(PORa^l87iFANr zZYyzZO@?K~5+bD?+Aye=2RJL$%NeDM`O2ShoK9S-#1Q+R>8p^!Hkst5cPhVhvIoF7 zh2iL#ho33W_+%RC##m+8;>bjn7Rr;EmX+1&g})h#)6RqP4KnMqK+UGHxen*Tzlj_& zUcCtyK)KiUZpJ@r0OU1x>2(Cr;`(KYDj)h~7lw>Vn&;y1@su&|$q2|Vhny$nG-=H6 zpeL0MDv%UbvI^I+&qsU9nKihz6X>Q`q%$YI(uzTGZWi7%&cf;H?zIF-moB4?=r>60 z%Jo?5u~TcT&i+)H@0C>*<*6iBu-o1KQ~4lC z!{%z>ld+rL1EBKAQ8T)OiHeaC2sLw^XN#Mtv734nwW?_FE#23GuM1-hF67Ovl92tN zl9ooF;m|-L>wZ9es)QIq{W0mQDH#_=5#qITfy) z)S4N1!WzlonKmEA5Cf@|AN;&l@5Atc^6Lk5+qLLmYL0>z(~K91^=0&;I-dhHx~54V zhZSZ*nST*8&u7Fh+PoMHaNSwrh~^!68ZsgyGUjt@Tv>II$s&#u*Ldu7IFo%(rn_{w zwygqoCqRMgQdSRYVt$3-kFw)JSoL5%81#$+b>owEJXiuHnGP!Pl%dopF=Q6hHwuVk zuwuYOs0A`zGQrS|yY%KP$%ITG8iC^^e={Zmne)A?KWQ#0^8r)!zRpbCB9x@yiF8}z z4#@o4&;%GlI1c2I1EeN95%kU0wE_h_V>!_xHsBMlyp@4bc^X?iSE2|7w!eWnQ~giT zs(_$#Mx=|$u*_{3_A*mrh4UfI%@5I(?uOa;-!&q*LgNqOcWrz_(&D%#y=%G&hnH}v z0hDc>4){Yn5j_WP1y5fD{t|GX-)t_xo*-EWo84Y_Dak?VSrOwv;B)tMC_8R6e(+_7 z$^aA&kaIP&L|AV#tfqqL9$2m@=Gn;qAY%t29hhuL$!o--l&;pLA2UT{Ny!wmNI=w< zM?_U;Ln-o|hO`WeSBJJM(rnH$n2Ii0jhdK|F_F<5lNa8oh~mYBX&Ttg#)w7Ho(;~Z ze|4zO(liiqfZ<8XZkmB(v90gelCFqM*aAqm)(w=9DuvGQP4Tce*p>}d&t)dG!09hQ zDi~?#ePhIf?xNv}s0(36Csfbb9Z>MAZ8eMZ^@kUF!0;^uOx;3-U^Y4Re1oXl-azENPvl;rQzriVR%RsLOe)$(XC)W{> z;$Z|D**YVsMkSJG4UsFbAIW;CQ`ro`@~Og~z)E#Rt|-JqZgV|OB( zS)=PS9H4S&oP76$RMwv{Jfmq$Oa;uoQM91Qo0QzGgqehTx13zp)OrmhTM(ciDF@no z^9S3#QXeHhJ5)n)lFJ(AOKv)d`qRwLUE;aye1;azJi~}2Y%M$btwFgwcoL;~HCSxT zP%f|>HheJ|3;L_*G>9CC-&&JG#-kMlPnhpi)P?TOmMAK@%->DsT%A?;I{{_WqE0c; zmw9H%fBGL^`r-6U#8()r))Gs}h8(1!9g4_eiFlrT_- zmR*O2m%i(jsPck}&%4lI#Mi_DFuVZHPw*{XFCt>ZzQ`xA*tVsCGG z$p`M#H%%5vd319!!=t@Imf76pVWv5OJnC@p5d;RemUdvO-T~C{GRrDfPtyqR$jw?ekgrqe4JArP4WhSgbHuro87GvaPgpIl;!;hYdtJ1tI&aZ8dFwU=1!)fS%fZsHOl}Ea=xuAC3TKYdX;F&UO`_Qqd%H)S62fDO@IVf-S54=>4%^B`*mGeUs!YO9i*Id<~Zl~%Sr zBVCrhQrtDTR}@zf_S@jVU6_Db5Dul<1`c!e-}C;c%^d|yb1z;t?%$^kKZQik_gF0B znh7xgY6-(fZ?+OLvlP1@n?1cI>EA;8O>MkT7=RNz2m?;2E8R>(4oF0L^-3&@qiU)~ z_>YF$4AT4+$n63Dc88CjVB`@&lv>)fO4Liv=++6M3eAF7QEb}MUzi$_gnlqEMMwY) zJ00kR1DKlvtBp4xdX{(Ei$-h7A&F@x9sRo&TD8_#r#bq#<`Kt>H)x&w*{~6dZQ-Rk z&Hb~phL^`zTaesbVO`1&JQTfV(MkeCyP_)rV<*g-4B(ys2ZBGSSPHeZu`nKd_9hl{ z`gsI*ZtP$ENVS6h^y9XTXZWOchR5f%IN3YaDhH0G5pGs4zq>0fXSXt-C`OUe6Ox^w*)@}@OL>)JOhboH&Ivl1wuG8cNBa8qqxt2 z`vXJA4K@q%K){|S`lhky6{5XbJ%2?bbn!El;_H~3ROMmwTa39|N$W}NrH2~RQ}qJo zVYjP%+M7^29+ZDuExx}m_tV?AC(He=+juQbDX6AoM#G)h+8HR5I1uMbGt7@{)#rx_ zK#dkFc-^b20bLZKs2Xt}v3K)n(MqFA!Bpam*qlt^zPBK$NC9(ZUHuwLrC}8He)+Vb z6RI%`o<4bg*OFI7Qv&>qSCr*;j55VpB>J<-!gucjdLA;9nWC1B;7r z9BuxD?Vjsc&OnsIRv3~FE8AIOks7a|3Fz1##OcDN)$5hyKJ7 zi5shP8C@T>;`s7E3-tSTV`5V+qS3f6+=1Uz&8~y}@^V)T=7l2Yss(3(R zkiJxaT_r=;6IZY-P=#3Ul8A6&1~QE<>`7v0m{JtFd#q`*$@Hl;>IY$9VN>Mi3M_AD zF>Jg3=S|Ho2jhp?IX;a9#7rX>r#tMuISAiBb>tDdxGkJi+ZhJyZ70G4l?DCKg!ouK z^^ZANsX?aplVaq-%X=}0y#TIdVqPt8-T+>31&myhhj3mmq$B4Aj!hIsF5YTWo!7?4 zw3^bcEs^dRDHf0U#=TXvWyi*0%2l)Aa z-?Sg3I<|bPZG|Ck-1$pC5ESsND08ez=est!O%i@fD`|oyg?ttD7(h|7(7F<#zAtn_ zDh`wUS(R?jy&=uCr#8os=C8avnytcWtU^))~61)&;JV7){D9oZzH)8kJ2RXjozUyXy#yN=k|) zG>lH5IxX13AC(7haWv;~t?lr*6FPQ>@sMivA2gE4pP=|jCq`&~(2vU%4VCm~s}}5b zqu2BP-%?X(3nBnug(Op*b=9&R4?>H;O{HgSWG4FLqdCihGld~_JOY8w+}vF-BqMpg z(7hLa2<$Cdo2p)`!-KPr_qBvRmIQQK>%+6;P&KunN=~*`JX;>>9mAI-C_ zxKf~?JyTrQ8w-<};vMYCQvPCj$6(Z1nWuP+;?eNVOAoNRfikgvP$U+YBco6Ch%3_ry2DKbDo6dI$8wAtVkGQqIU-A074qIsGfJRzLR#~EA^}b2 zC4^d=0(LQj<(4n1x=r>dmzl!SoUXKzeJjo7DcEUeG*hLKhV zFmTZ7B7s6zm!&&w_E2?ANDS#K0N&zKTa?^KrPcHUY{@4@YOpf*!^hT2y)eVo`Ti9? zkD2!=i>A3{il@Xx+wb%jzJTsA$HAi$gTMA@697wQwHT7W&YM3Z;>0@-QX}x>pp%q@ z#rDY=B)IR7;&AQUYg_cW+Gvjr-T!>`!5VCYVPD4bd!0nYyc0<1Jer7N)Q-Ff_xEq3 zvB0prYO%C;4KSR?0~Y&SP+^HD#WZ~qFvbSZO6HH|saNC<+{h_^E=RhRKmP>BvhXU9 zhXZU?q5}hg$vjT&yEeGkw|)1vRC~Eld8U@5kyS${>JO0Sa<7s66L@*m)N5l%_nr2t zL@K^X4fqPMY5$S`C+7 zr8TCQOC-WIjFyA};R?dpHnuTfd!i!zoA5p_Ed2F4@>vI#W+^R2|=Sw)2Y#F3lLem{h1Cw%H5C5}V_?Knlv_xPt^5ht;` zDG9aig(Jo7*XMl4b<}u>CiY*Ur?0+C!=0#Nr`=gp5CaCHx~GWb5n>(0z4l0YT9KsZ zEe#ca?Nj#PgzF7ULty*m0zJHs+<$U>C-4(5VYA!mnEnX~4L9!s4`T>;bEVB$juX|2 z6JYYH0TrR5B^It9!}Sy%90`YtXdXhHa$1ek!DB}{ac)RwcArP_89bgcU+m*Z4W72i zb-5M*&@1xj@@LR~r)3=DB@|ak96o&n;ERrb<8k}GiWo4 zl46fCk7`vykWei$CREVN!jcdxeE>GB#S2GrrJa9T)%3LkI(q23$F!dqgXzd$9{VN{Iv@A zg!nS|#Fi?<&=d7ekhuJ{YsjsR;YOqpS!}UmIBwDo{(=i8RRi?hI?aXWaOvoQ!*p*y zUIr98sUJ(LTg}oIHlV!gtqN1#^;4#QyjC6oFw2xd3^t7X$*~8L0M>`QzJa?SM_peH zlRZG#oR%J7E3NIIGT~U@m@K9XY8?VW(T^1$|KM-v--EdAQiq36exS8Jd>YZZKDg`{ znvNpGzJ)(=?48C=6h^s4dbV*8hzE`lyqY*Gg1}2<|DHmS3t6hQ>Rk)> z1gI^brdQ-q9Y)V6oROQscX(re2REyYGq9xw6+Z_E!DqOrcwBw!5y*QLy2wK7E|1nP z^ebH{(OQPqp*L;JQV$aCLOP3tWz`f!CU0p?OPnQm)&f(Nf(t>LO7C`uqQRg{^-7 zZ?V;{jVG}B-HLdjY%WY}Kd2{j-~VLT+^-=XI9y;L-o*PqU=|co&Lmu-++QTLKECHE z2I-|*MgXVVlgX;pDrW`3)EbYrx7rmbd}mKYFMNNOE|r`GlAZ6{!C0gE09b=x{JibK z8E!rOG_4TpHDbnp8Wp6a3)s@&FzNbAHE8>EMyh`sZM>942~VnK>A_K>ptK(|WIR7~ zloFo9wMaLGT=?M-O3L8?nH}1eFr(6#dsVMAHnIh^(ukWDEJKjN*&sn2WQk-QQ(F|h zpeTAqSPq6dRhuVnQ4gv;1+E(oIA8gkVe7l;}$IYzB7sW-;I9yR{?TQ%Iurd?>eKc;d+A7(j_ z&tEX~1MVbSPcBE>bWBKxIcp_}1?rpeTF~h=@@r9qqOyR~YqGLH?pAm} zC$#FnxbB%Xx6qScc`K@Q-#Ub!jHNn<1Xs2}odnroj(#(1+m|jyaQqCIXS3%|+ zGknaSP>_|fhlhr2qz7OWeVYIC#R(Z0w`bf$$}Q4=c;E629Nu>9S1atj$Ii3&UfT#3 z-uu7=@>gCEv3d2_`_BV-4EJEn;>aJijzM#@gGO6&;bBzRf0o;S$p?TMsgN!Py@TN2 zf@Gl#*N{sK(MhP39Iv(tCX58wII^3cIH8{iqBxe*!sFL2?u@&IwLvr&?uFTcd(y^y z8?5Yq#Gp-a+0N74KcNa!;-65QK*Kw4#l@IgbzUFtm~Mf?`A0FgdP0qWdYa6rFz7_u zGJHh`(6?ukY1P<+$%Atj->v6V!{M3V#Nv2d3Tz*pa7F!&PsTTj8ut3M( zu;cNsbYs-xjrm-G^_Ag$XC_XZ93@M`B0B>UlaTcVyX`ZFMh=gU+FVXQi~78sSKIak z%{TBLm27FGPHB#Uq~erIIkbemgMe_@V+XE1gjqtu)-4OC*$^ASbD`4+b)iU~S9kz_ z9yZ%Rf;=-X%^#UP&d5pML)j(j^_6%Tb_m~%BY6GI-eclN20 zb0b!V&5hs#BgI}^0qYeTW`LorlI%yMNX53(U;PDci8(_2AKU>GCVn1GNoMMQ1?vZB zA(&=;mzlP-s+X&`43D!1K=IF@JqM>JhMI<=2;gmXpv{KcxDhjCZ0q{wKggf z4*>fKihjJf;f+1|_h9U${>xXSis_5-jRM#%k3&eQayBb#UZ)btGqbe1i>6i3eYs=@ z@H_Bo#hAaYy6IEGa)fx9wJKhJHT5VttyVEw;JO^m^ z1VsRKxt*3uBl@b^a;30gpW%7H|>~5^z-6 zv%^{7cFKn(8*{m36XWh#H~5(w@U2YAI?e?k<*d%zBl`~!$nfz_=GJfN=K_f* z+*u{@dJ@anR-oU*cE4_aaX;*LY8`*Hr|7esL$;raz{HhDF@54{tcL3HC_K4~W0XN& z3H>L99|iTL1U3Yvu^Ik0iwc_7lCq(>u_w9UjN$=GS_=~EriG4DhfEB|rA za)kJ2d^P%1jUG7lS=^v*!HyK<5AQ>~FooKyaxv+0Qgz=D>*5f9YS8iEl#+SwoXpMb z8=Bm6?M?R&sr&X}Y9z1TH`KV^eFz37M?M6mjDJ0#set_material_params(mat_inp); + + // Set 1 keV cutoff + CutoffParams::Input cut_inp; + cut_inp.materials = this->material_params(); + cut_inp.particles = this->particle_params(); + cut_inp.cutoffs = {{pdg::positron(), {{MevEnergy{0.001}, 0.1234}}}}; + this->set_cutoff_params(cut_inp); + + // Construct model + auto imported = std::make_shared( + this->imported_data().processes); + model_ = std::make_shared( + ActionId{0}, + *this->particle_params(), + imported, + this->imported_data().mu_pair_production_data); + + // Set default particle to 10 GeV muon + this->set_inc_particle(pdg::mu_minus(), MevEnergy{1e4}); + this->set_inc_direction({0, 0, 1}); + this->set_material("Cu"); + } + + void sanity_check(Interaction const& interaction) const + { + // Check change to parent track + EXPECT_GT(this->particle_track().energy().value(), + interaction.energy.value()); + EXPECT_LT(0, interaction.energy.value()); + EXPECT_SOFT_EQ(1.0, norm(interaction.direction)); + EXPECT_EQ(Action::scattered, interaction.action); + + // Check secondaries + ASSERT_EQ(2, interaction.secondaries.size()); + auto const& electron = interaction.secondaries[0]; + EXPECT_TRUE(electron); + EXPECT_GT(this->particle_track().energy(), electron.energy); + EXPECT_LT(zero_quantity(), electron.energy); + EXPECT_SOFT_EQ(1.0, norm(electron.direction)); + EXPECT_EQ(model_->host_ref().ids.electron, electron.particle_id); + + auto const& positron = interaction.secondaries[1]; + EXPECT_TRUE(positron); + EXPECT_GT(this->particle_track().energy(), positron.energy); + EXPECT_LT(zero_quantity(), positron.energy); + EXPECT_SOFT_EQ(1.0, norm(positron.direction)); + EXPECT_EQ(model_->host_ref().ids.positron, positron.particle_id); + + // Check conservation between primary and secondaries + // this->check_conservation(interaction); + this->check_energy_conservation(interaction); + } + + //! \note These tests use a trimmed element table + std::string_view geometry_basename() const final + { + return "four-steel-slabs"; + } + + SPConstTrackInit build_init() override { CELER_ASSERT_UNREACHABLE(); } + SPConstAction build_along_step() override { CELER_ASSERT_UNREACHABLE(); } + + protected: + std::shared_ptr model_; +}; + +//---------------------------------------------------------------------------// +// TESTS +//---------------------------------------------------------------------------// + +TEST_F(MuPairProductionTest, distribution) +{ + int num_samples = 10000; + int num_bins = 12; + + real_type two_me + = 2 * value_as(model_->host_ref().electron_mass); + + // Get view to the current element + auto element + = this->material_track().make_material_view().make_element_view( + ElementComponentId{0}); + + // Get the production cuts + auto cutoff = this->cutoff_params()->get(MaterialId{0}); + + RandomEngine& rng = InteractorHostBase::rng(); + + std::vector counters; + std::vector min_energy; + std::vector max_energy; + std::vector avg_energy; + std::vector avg_energy_fraction; + for (real_type energy : {1e3, 1e4, 1e5, 1e6, 1e7}) + { + this->set_inc_particle(pdg::mu_minus(), MevEnergy(energy)); + + MuPPEnergyDistribution sample( + model_->host_ref(), this->particle_track(), cutoff, element); + real_type min = value_as(sample.min_pair_energy()) - two_me; + real_type max = value_as(sample.max_pair_energy()) - two_me; + + real_type sum_energy = 0; + real_type energy_fraction = 0; + std::vector count(num_bins); + for ([[maybe_unused]] int i : range(num_samples)) + { + // TODO: test energy partition + auto energy = sample(rng); + auto r = value_as(energy.electron + energy.positron); + ASSERT_GE(r, min); + ASSERT_LE(r, max); + int bin = int(std::log(r / min) / std::log(max / min) * num_bins); + CELER_ASSERT(bin >= 0 && bin < num_bins); + ++count[bin]; + sum_energy += r; + energy_fraction += value_as(energy.electron) / r; + } + counters.insert(counters.end(), count.begin(), count.end()); + min_energy.push_back(min); + max_energy.push_back(max); + avg_energy.push_back(sum_energy / num_samples); + avg_energy_fraction.push_back(energy_fraction / num_samples); + } + + static int const expected_counters[] = { + 162, 1066, 2154, 2691, 2020, 1115, 518, 196, 60, 13, 5, 0, + 270, 931, 1869, 2496, 2103, 1514, 534, 212, 51, 15, 5, 0, + 208, 811, 1608, 2146, 2099, 1668, 947, 387, 101, 20, 3, 2, + 203, 782, 1564, 1987, 2015, 1717, 1058, 489, 161, 16, 8, 0, + 197, 767, 1548, 1948, 1965, 1607, 1116, 605, 200, 43, 4, 0, + }; + static double const expected_min_energy[] = { + 1.0219978922, + 1.0219978922, + 1.0219978922, + 1.0219978922, + 1.0219978922, + }; + static double const expected_max_energy[] = { + 703.23539643546, + 9703.2353964355, + 99703.235396435, + 999703.23539644, + 9999703.2353964, + }; + static double const expected_avg_energy[] = { + 11.551429194638, + 42.5605253686, + 216.32003549047, + 1093.2367878798, + 6041.1513546243, + }; + static double const expected_avg_energy_fraction[] = { + 0.50427974126835, + 0.50112476955009, + 0.49759901188728, + 0.50543113944062, + 0.50102592483879, + }; + EXPECT_VEC_EQ(expected_counters, counters); + EXPECT_VEC_SOFT_EQ(expected_min_energy, min_energy); + EXPECT_VEC_SOFT_EQ(expected_max_energy, max_energy); + EXPECT_VEC_SOFT_EQ(expected_avg_energy, avg_energy); + EXPECT_VEC_SOFT_EQ(expected_avg_energy_fraction, avg_energy_fraction); +} + +TEST_F(MuPairProductionTest, basic) +{ + // Reserve 8 secondaries, two for each sample + int const num_samples = 4; + this->resize_secondaries(2 * num_samples); + + // Get view to the current element + auto element + = this->material_track().make_material_view().make_element_view( + ElementComponentId{0}); + + // Get the production cuts + auto cutoff = this->cutoff_params()->get(MaterialId{0}); + + // Create the interactor + MuPairProductionInteractor interact(model_->host_ref(), + this->particle_track(), + cutoff, + element, + this->direction(), + this->secondary_allocator()); + RandomEngine& rng = InteractorHostBase::rng(); + + std::vector pair_energy; + std::vector costheta; + + // Produce four samples from the original incident energy + for (int i : range(num_samples)) + { + Interaction result = interact(rng); + SCOPED_TRACE(result); + this->sanity_check(result); + + EXPECT_EQ(result.secondaries.data(), + this->secondary_allocator().get().data() + + result.secondaries.size() * i); + + pair_energy.push_back(value_as( + result.secondaries[0].energy + result.secondaries[1].energy)); + costheta.push_back(dot_product(result.secondaries[0].direction, + result.secondaries[1].direction)); + } + + EXPECT_EQ(2 * num_samples, this->secondary_allocator().get().size()); + + // Note: these are "gold" values based on the host RNG. + static double const expected_pair_energy[] = { + 5.1981351222035, + 21.411122079708, + 39.340205211007, + 1.2067098240449, + }; + static double const expected_costheta[] = { + 0.99992128683238, + 0.97331314773255, + 0.9996196536095, + 0.99925389709579, + }; + EXPECT_VEC_SOFT_EQ(expected_pair_energy, pair_energy); + EXPECT_VEC_SOFT_EQ(expected_costheta, costheta); + + // Next sample should fail because we're out of secondary buffer space + { + Interaction result = interact(rng); + EXPECT_EQ(0, result.secondaries.size()); + EXPECT_EQ(Action::failed, result.action); + } +} + +TEST_F(MuPairProductionTest, stress_test) +{ + unsigned int const num_samples = 10000; + std::vector avg_engine_samples; + std::vector avg_electron_energy; + std::vector avg_positron_energy; + std::vector avg_costheta; + + // Get view to the current element + auto element + = this->material_track().make_material_view().make_element_view( + ElementComponentId{0}); + + // Get the production cuts + auto cutoff = this->cutoff_params()->get(MaterialId{0}); + + for (real_type inc_e : {1e3, 1e4, 1e5, 1e6, 1e7}) + { + SCOPED_TRACE("Incident energy: " + std::to_string(inc_e)); + this->set_inc_particle(pdg::mu_minus(), MevEnergy{inc_e}); + + RandomEngine& rng = InteractorHostBase::rng(); + RandomEngine::size_type num_particles_sampled = 0; + double electron_energy = 0; + double positron_energy = 0; + double costheta = 0; + + // Loop over several incident directions + for (Real3 const& inc_dir : + {Real3{0, 0, 1}, Real3{1, 0, 0}, Real3{1e-9, 0, 1}, Real3{1, 1, 1}}) + { + SCOPED_TRACE("Incident direction: " + to_string(inc_dir)); + this->set_inc_direction(inc_dir); + this->resize_secondaries(2 * num_samples); + + // Create the interactor + MuPairProductionInteractor interact(model_->host_ref(), + this->particle_track(), + cutoff, + element, + this->direction(), + this->secondary_allocator()); + + // Loop over many particles + for (unsigned int i = 0; i < num_samples; ++i) + { + Interaction result = interact(rng); + this->sanity_check(result); + + electron_energy + += value_as(result.secondaries[0].energy); + positron_energy + += value_as(result.secondaries[1].energy); + costheta += dot_product(result.secondaries[0].direction, + result.secondaries[1].direction); + } + EXPECT_EQ(2 * num_samples, + this->secondary_allocator().get().size()); + num_particles_sampled += num_samples; + } + avg_engine_samples.push_back(real_type(rng.count()) + / num_particles_sampled); + avg_electron_energy.push_back(electron_energy / num_particles_sampled); + avg_positron_energy.push_back(positron_energy / num_particles_sampled); + avg_costheta.push_back(costheta / num_particles_sampled); + } + + // Gold values for average number of calls to RNG + static double const expected_avg_engine_samples[] = {10, 10, 10, 10, 10}; + static double const expected_avg_electron_energy[] = { + 5.9452433822303, + 20.776282536509, + 98.201429477115, + 555.92710681765, + 2855.9810205079, + }; + static double const expected_avg_positron_energy[] = { + 5.8651456808897, + 21.483133310816, + 100.92564951414, + 546.95048450797, + 2824.7431627774, + }; + static double const expected_avg_costheta[] = { + 0.94178280008365, + 0.99880165151034, + 0.99998776687485, + 0.99999983141391, + 0.99999999832285, + }; + EXPECT_VEC_SOFT_EQ(expected_avg_engine_samples, avg_engine_samples); + EXPECT_VEC_SOFT_EQ(expected_avg_electron_energy, avg_electron_energy); + EXPECT_VEC_SOFT_EQ(expected_avg_positron_energy, avg_positron_energy); + EXPECT_VEC_SOFT_EQ(expected_avg_costheta, avg_costheta); +} + +//---------------------------------------------------------------------------// +} // namespace test +} // namespace celeritas diff --git a/test/celeritas/em/distribution/MuBremsPPAngularDistribution.test.cc b/test/celeritas/em/distribution/MuAngularDistribution.test.cc similarity index 88% rename from test/celeritas/em/distribution/MuBremsPPAngularDistribution.test.cc rename to test/celeritas/em/distribution/MuAngularDistribution.test.cc index 28b4894f4b..fdab5e59ef 100644 --- a/test/celeritas/em/distribution/MuBremsPPAngularDistribution.test.cc +++ b/test/celeritas/em/distribution/MuAngularDistribution.test.cc @@ -3,9 +3,9 @@ // See the top-level COPYRIGHT file for details. // SPDX-License-Identifier: (Apache-2.0 OR MIT) //---------------------------------------------------------------------------// -//! \file celeritas/em/distribution/MuBremsPPAngularDistribution.test.cc +//! \file celeritas/em/distribution/MuAngularDistribution.test.cc //---------------------------------------------------------------------------// -#include "celeritas/em/distribution/MuBremsPPAngularDistribution.hh" +#include "celeritas/em/distribution/MuAngularDistribution.hh" #include @@ -19,7 +19,7 @@ namespace celeritas namespace test { //---------------------------------------------------------------------------// -TEST(MuBremsPPAngularDistributionTest, costheta_dist) +TEST(MuAngularDistributionTest, costheta_dist) { using Energy = units::MevEnergy; using Mass = units::MevMass; @@ -33,7 +33,7 @@ TEST(MuBremsPPAngularDistributionTest, costheta_dist) { for (real_type eps : {0.001, 0.01, 0.1}) { - MuBremsPPAngularDistribution sample_costheta( + MuAngularDistribution sample_costheta( Energy{inc_e}, muon_mass, Energy{eps * inc_e}); real_type costheta_sum = 0; diff --git a/test/celeritas/ext/GeantImporter.test.cc b/test/celeritas/ext/GeantImporter.test.cc index 48bd047f6c..94474ad0fa 100644 --- a/test/celeritas/ext/GeantImporter.test.cc +++ b/test/celeritas/ext/GeantImporter.test.cc @@ -1123,18 +1123,6 @@ TEST_F(FourSteelSlabsEmStandard, mu_pair_production_data) real_type const tol = geant4_version < Version(11, 1, 0) ? 1e-12 : 0.03; static double const expected_table_x[] = { - -6.1928487397154, - 0, - -6.1928487397154, - 0, - -6.1928487397154, - 0, - -6.1928487397154, - 0, - -6.1928487397154, - 0, - }; - static double const expected_table_y[] = { 6.9077552789821, 18.420680743952, 6.9077552789821, @@ -1146,6 +1134,18 @@ TEST_F(FourSteelSlabsEmStandard, mu_pair_production_data) 6.9077552789821, 18.420680743952, }; + static double const expected_table_y[] = { + -6.1928487397154, + 0, + -6.1928487397154, + 0, + -6.1928487397154, + 0, + -6.1928487397154, + 0, + -6.1928487397154, + 0, + }; static double const expected_table_value[] = { 0, 0.24363843626056, diff --git a/test/celeritas/ext/RootImporter.test.cc b/test/celeritas/ext/RootImporter.test.cc index 7b2e890086..20ad50f58f 100644 --- a/test/celeritas/ext/RootImporter.test.cc +++ b/test/celeritas/ext/RootImporter.test.cc @@ -151,7 +151,7 @@ TEST_F(RootImporterTest, phys_materials) TEST_F(RootImporterTest, processes) { auto const& processes = this->imported_data().processes; - EXPECT_EQ(15, processes.size()); + EXPECT_EQ(17, processes.size()); auto find_process = [&processes](PDGNumber pdg, ImportProcessClass ipc) { return std::find_if(processes.begin(), diff --git a/test/celeritas/ext/RootJsonDumper.test.cc b/test/celeritas/ext/RootJsonDumper.test.cc index 3f0ad37c4a..15fc3cfcfe 100644 --- a/test/celeritas/ext/RootJsonDumper.test.cc +++ b/test/celeritas/ext/RootJsonDumper.test.cc @@ -42,6 +42,7 @@ TEST_F(RootJsonDumperTest, all) ImportDataTrimmer::Input inp; inp.materials = true; inp.physics = true; + inp.mupp = true; inp.max_size = 2; ImportDataTrimmer trim{inp}; trim(imported); @@ -170,8 +171,6 @@ TEST_F(RootJsonDumperTest, all) "range" : 0.1 }}] }], -"optical_models" : [], -"optical_materials" : [], "regions" : [{ "_typename" : "celeritas::ImportRegion", "name" : "DefaultRegionForTheWorld", @@ -325,8 +324,18 @@ TEST_F(RootJsonDumperTest, all) "atomic_relaxation_data" : [], "mu_pair_production_data" : { "_typename" : "celeritas::ImportMuPairProductionTable", - "atomic_number" : [], - "physics_vectors" : [] + "atomic_number" : [1, 92], + "physics_vectors" : [{ + "_typename" : "celeritas::ImportPhysics2DVector", + "x" : [6.90775527898214, 18.4206807439524], + "y" : [-6.19284873971536, 0], + "value" : [0, 4.0853712905423e-28, 0, 2.43638436260562e-25] + }, { + "_typename" : "celeritas::ImportPhysics2DVector", + "x" : [6.90775527898214, 18.4206807439524], + "y" : [-6.19284873971536, 0], + "value" : [0, 3.30246663127583e-24, 0, 7.93413967608228e-22] + }] }, "em_params" : { "_typename" : "celeritas::ImportEmParameters", @@ -375,6 +384,8 @@ TEST_F(RootJsonDumperTest, all) "_typename" : "celeritas::ImportOpticalParameters", "scintillation_by_particle" : false }, +"optical_models" : [], +"optical_materials" : [], "units" : "cgs" })json", str); diff --git a/test/celeritas/phys/InteractorHostTestBase.cc b/test/celeritas/phys/InteractorHostTestBase.cc index 263dc158df..e0e7d70991 100644 --- a/test/celeritas/phys/InteractorHostTestBase.cc +++ b/test/celeritas/phys/InteractorHostTestBase.cc @@ -24,7 +24,7 @@ namespace test /*! * Initialize secondary allocation on construction. */ -InteractorHostTestBase::InteractorHostTestBase() +InteractorHostBase::InteractorHostBase() { using namespace constants; using namespace units; @@ -117,13 +117,13 @@ InteractorHostTestBase::InteractorHostTestBase() /*! * Default destructor. */ -InteractorHostTestBase::~InteractorHostTestBase() = default; +InteractorHostBase::~InteractorHostBase() = default; //---------------------------------------------------------------------------// /*! * Helper to make dummy ImportProcess data . */ -ImportProcess InteractorHostTestBase::make_import_process( +ImportProcess InteractorHostBase::make_import_process( PDGNumber particle, PDGNumber secondary, ImportProcessClass ipc, @@ -159,7 +159,7 @@ ImportProcess InteractorHostTestBase::make_import_process( /*! * Set particle parameters. */ -void InteractorHostTestBase::set_material_params(MaterialParams::Input inp) +void InteractorHostBase::set_material_params(MaterialParams::Input inp) { CELER_EXPECT(!inp.materials.empty()); @@ -172,7 +172,7 @@ void InteractorHostTestBase::set_material_params(MaterialParams::Input inp) /*! * Initialize the incident track's material */ -void InteractorHostTestBase::set_material(std::string const& name) +void InteractorHostBase::set_material(std::string const& name) { CELER_EXPECT(material_params_); @@ -190,7 +190,7 @@ void InteractorHostTestBase::set_material(std::string const& name) /*! * Set particle parameters. */ -void InteractorHostTestBase::set_particle_params(ParticleParams::Input inp) +void InteractorHostBase::set_particle_params(ParticleParams::Input inp) { CELER_EXPECT(!inp.empty()); particle_params_ = std::make_shared(std::move(inp)); @@ -202,7 +202,7 @@ void InteractorHostTestBase::set_particle_params(ParticleParams::Input inp) /*! * Set cutoff parameters. */ -void InteractorHostTestBase::set_cutoff_params(CutoffParams::Input inp) +void InteractorHostBase::set_cutoff_params(CutoffParams::Input inp) { CELER_EXPECT(inp.materials && inp.particles); cutoff_params_ = std::make_shared(std::move(inp)); @@ -212,7 +212,7 @@ void InteractorHostTestBase::set_cutoff_params(CutoffParams::Input inp) /*! * Set imported processes. */ -void InteractorHostTestBase::set_imported_processes( +void InteractorHostBase::set_imported_processes( std::vector inp) { CELER_EXPECT(!inp.empty()); @@ -223,7 +223,7 @@ void InteractorHostTestBase::set_imported_processes( /*! * Initialize the incident particle data */ -void InteractorHostTestBase::set_inc_particle(PDGNumber pdg, MevEnergy energy) +void InteractorHostBase::set_inc_particle(PDGNumber pdg, MevEnergy energy) { CELER_EXPECT(particle_params_); CELER_EXPECT(pdg); @@ -244,7 +244,7 @@ void InteractorHostTestBase::set_inc_particle(PDGNumber pdg, MevEnergy energy) /*! * Set an incident direction (and normalize it). */ -void InteractorHostTestBase::set_inc_direction(Real3 const& dir) +void InteractorHostBase::set_inc_direction(Real3 const& dir) { CELER_EXPECT(norm(dir) > 0); @@ -255,7 +255,7 @@ void InteractorHostTestBase::set_inc_direction(Real3 const& dir) /*! * Resize secondaries. */ -void InteractorHostTestBase::resize_secondaries(int count) +void InteractorHostBase::resize_secondaries(int count) { CELER_EXPECT(count > 0); secondaries_ = StateStore(count); @@ -266,7 +266,7 @@ void InteractorHostTestBase::resize_secondaries(int count) /*! * Check for energy and momentum conservation in the interaction. */ -void InteractorHostTestBase::check_conservation(Interaction const& interaction) const +void InteractorHostBase::check_conservation(Interaction const& interaction) const { ASSERT_NE(interaction.action, Action::failed); @@ -278,7 +278,7 @@ void InteractorHostTestBase::check_conservation(Interaction const& interaction) /*! * Check for energy conservation in the interaction. */ -void InteractorHostTestBase::check_energy_conservation( +void InteractorHostBase::check_energy_conservation( Interaction const& interaction) const { // Sum of exiting kinetic energy @@ -296,7 +296,8 @@ void InteractorHostTestBase::check_energy_conservation( exit_energy += s.energy.value(); // Account for positron production - if (s && s.particle_id == particle_params_->find(pdg::positron())) + if (s && s.particle_id == particle_params_->find(pdg::positron()) + && interaction.action == Action::absorbed) { exit_energy += 2 * particle_params_->get(s.particle_id).mass().value(); @@ -311,7 +312,7 @@ void InteractorHostTestBase::check_energy_conservation( /*! * Check for momentum conservation in the interaction. */ -void InteractorHostTestBase::check_momentum_conservation( +void InteractorHostBase::check_momentum_conservation( Interaction const& interaction) const { CollectionStateStore temp_store( diff --git a/test/celeritas/phys/InteractorHostTestBase.hh b/test/celeritas/phys/InteractorHostTestBase.hh index 591837d414..76ea1e8228 100644 --- a/test/celeritas/phys/InteractorHostTestBase.hh +++ b/test/celeritas/phys/InteractorHostTestBase.hh @@ -49,7 +49,7 @@ namespace test * \todo Since this now uses Collection objects it's generally safe to use this * to test Models as well as device code -- think about renaming it. */ -class InteractorHostTestBase : public Test +class InteractorHostBase { public: //!@{ @@ -64,8 +64,8 @@ class InteractorHostTestBase : public Test public: //!@{ //! Initialize and destroy - InteractorHostTestBase(); - ~InteractorHostTestBase(); + InteractorHostBase(); + ~InteractorHostBase(); //!@} // Helper to make dummy ImportProcess @@ -189,6 +189,10 @@ class InteractorHostTestBase : public Test std::shared_ptr sa_view_; }; +class InteractorHostTestBase : public InteractorHostBase, public Test +{ +}; + //---------------------------------------------------------------------------// } // namespace test } // namespace celeritas diff --git a/test/corecel/data/HyperslabIndexer.test.cc b/test/corecel/data/HyperslabIndexer.test.cc index 6a2589f819..b607c58b0e 100644 --- a/test/corecel/data/HyperslabIndexer.test.cc +++ b/test/corecel/data/HyperslabIndexer.test.cc @@ -30,6 +30,7 @@ TEST(HyperslabIndexerTest, 2D) { Array coords{a, b}; EXPECT_EQ(index, to_index(coords)); + EXPECT_EQ(index, to_index(a, b)); EXPECT_EQ(coords, to_coords(index)); index++; } @@ -51,6 +52,7 @@ TEST(HyperslabIndexerTest, 3D) { Array coords{a, b, c}; EXPECT_EQ(index, to_index(coords)); + EXPECT_EQ(index, to_index(a, b, c)); EXPECT_EQ(coords, to_coords(index)); index++; } @@ -75,6 +77,7 @@ TEST(HyperslabIndexerTest, 4D) { Array coords{a, b, c, d}; EXPECT_EQ(index, to_index(coords)); + EXPECT_EQ(index, to_index(a, b, c, d)); EXPECT_EQ(coords, to_coords(index)); index++; } @@ -98,6 +101,7 @@ TEST(HyperslabIndexerTest, 5D_with_ones) { Array coords{a, 0, b, 0, c}; EXPECT_EQ(index, to_index(coords)); + EXPECT_EQ(index, to_index(a, 0, b, 0, c)); EXPECT_EQ(coords, to_coords(index)); index++; } From bd30043704855a5cc2d842ee4a0f756ca649c77c Mon Sep 17 00:00:00 2001 From: Philippe Canal Date: Wed, 4 Dec 2024 16:53:30 -0600 Subject: [PATCH 08/15] Update Readme and Contributing doc (#1532) * Update link to contributing/development guidelines * Clarify Spack usage * Mention the citation data duplication --- CITATION.cff | 3 +++ CONTRIBUTING.rst | 2 +- README.md | 51 +++++++++++++++++++++++++++++++++++++----------- 3 files changed, 44 insertions(+), 12 deletions(-) diff --git a/CITATION.cff b/CITATION.cff index ddbd1b0cf2..7fdcb5418c 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -17,6 +17,9 @@ # doi = {10.1051/epjconf/202429511005} # } # +# Note: This information is also present in the README.md and needs to +# kept in sync. +# # (CFF): cff-version: 1.2.0 diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 2fc3fb986e..2226eecef2 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -118,7 +118,7 @@ Congratulations! .. [resol] When you've fully implemented the reviewer's comment, you may mark it as resolved without commenting. Do not resolve a conversation if you - disagree with the feed: instead, post your view in a follow-on comment and + disagree with the feedback: instead, post your view in a follow-on comment and wait for the reviewer to respond. If you comment, whether to supplement your change or to iterate with the reviewer, please do not resolve the conversation since that makes it hard to find your comment. diff --git a/README.md b/README.md index 26ba506add..0331e59b33 100644 --- a/README.md +++ b/README.md @@ -25,19 +25,47 @@ doc` (user) or `ninja doxygen` (developer). # Installation for applications The easiest way to install Celeritas as a library/app is with Spack: -- Follow the first two steps above to install [Spack][spack-start] and set up its CUDA usage. -- Install Celeritas with `spack install celeritas` -- Use `spack load celeritas` to add the installation to your `PATH`. +- Follow these steps to install [Spack][spack-start]. +```console +# Install Spack +git clone -c feature.manyFiles=true --depth=2 https://github.com/spack/spack.git +# Add Spack to the shell environment +# For bash/zsh/sh (See [spack-start] for other shell) +``` +- Install Celeritas with +```console +spack install celeritas +```console +- Add the Celeritas installation to your `PATH` with: +```console +spack load celeritas +``` To install a GPU-enabled Celeritas build, you might have to make sure that VecGeom is also built with CUDA support if installing `celeritas+vecgeom`, which is the default geometry. -To do so, set the following configuration: +To do so, set Spack up its CUDA usage: +```console +. spack/share/spack/setup-env.sh +# Set up CUDA +$ spack external find cuda +# Optionally set the default configuration. Replace "cuda_arch=80" +# with your target architecture +$ spack config add packages:all:variants:"cxxstd=17 +cuda cuda_arch=80" +``` +and install Celeritas with this configuration: +```console +$ spack install celeritas +``` +If Celeritas was installed with a different configuration do +```console +$ spack install --fresh celeritas +``` +If you need to set a default configuration ```console -# Replace cuda_arch=80 with your target architecture -$ spack config add packages:vecgeom:variants:"cxxstd=17 +cuda cuda_arch=80" $ spack install celeritas +cuda cuda_arch=80 ``` + Then see the "Downstream usage as a library" section of the [installation documentation][install] for how to use Celeritas in your application or framework. @@ -62,7 +90,7 @@ $ spack external find cuda # Install celeritas dependencies $ spack env create celeritas scripts/spack.yaml $ spack env activate celeritas -$ spack config add packages:all:variants:"cxxstd=17 +cuda cuda_arch=70" +$ spack config add packages:all:variants:"cxxstd=17 +cuda cuda_arch=80" $ spack install # Configure, build, and test $ ./build.sh base @@ -97,12 +125,11 @@ Geant4: Since we compile with extra warning flags and avoid non-portable code, most other compilers *should* work. -The full set of configurations is viewable on CI platforms ([Jenkins][jenkins] and [GitHub Actions][gha]). +The full set of configurations is viewable on CI platform [GitHub Actions][gha]). Compatibility fixes that do not cause newer versions to fail are welcome. [spack]: https://github.com/spack/spack [install]: https://celeritas-project.github.io/celeritas/user/main/installation.html -[jenkins]: https://cloud.cees.ornl.gov/jenkins-ci/job/celeritas/job/develop [gha]: https://github.com/celeritas-project/celeritas/actions # Development @@ -111,8 +138,8 @@ See the [contribution guide][contributing-guidelines] for the contribution proce [the development guidelines][development-guidelines] for further details on coding in Celeritas, and [the administration guidelines][administration-guidelines] for community standards and roles. -[contributing-guidelines]: https://celeritas-project.github.io/celeritas/user/appendix/development.html#contributing-to-celeritas -[development-guidelines]: https://celeritas-project.github.io/celeritas/user/appendix/development.html#code-development-guidelines +[contributing-guidelines]: https://celeritas-project.github.io/celeritas/user/development/contributing.html +[development-guidelines]: https://celeritas-project.github.io/celeritas/user/development/coding.html [administration-guidelines]: https://celeritas-project.github.io/celeritas/user/appendix/administration.html # Directory structure @@ -130,6 +157,8 @@ details on coding in Celeritas, and [the administration guidelines][administrati # Citing Celeritas + + If using Celeritas in your work, we ask that you cite the following article: > Johnson, Seth R., Amanda Lund, Philippe Canal, Stefano C. Tognini, Julien Esseiva, Soon Yung Jun, Guilherme Lima, et al. 2024. “Celeritas: Accelerating Geant4 with GPUs.” EPJ Web of Conferences 295:11005. https://doi.org/10.1051/epjconf/202429511005. From 6f376a254a8e90fbc4fc5465b3ded5a3cf58b207 Mon Sep 17 00:00:00 2001 From: Julien Esseiva Date: Wed, 4 Dec 2024 15:56:39 -0800 Subject: [PATCH 09/15] Tidy headers (#1534) --- .clang-tidy | 10 +++++++++- app/celer-g4/ActionInitialization.cc | 2 +- app/celer-g4/ActionInitialization.hh | 2 +- app/celer-g4/DetectorConstruction.cc | 1 - app/celer-g4/RootIO.hh | 7 ------- app/celer-g4/SensitiveDetector.cc | 2 +- app/celer-g4/SensitiveDetector.hh | 4 ++-- src/accel/HepMC3PrimaryGenerator.hh | 1 + src/accel/RZMapMagneticField.hh | 3 ++- src/accel/SetupOptionsMessenger.hh | 2 +- src/accel/detail/HitProcessor.hh | 2 ++ src/accel/detail/LevelTouchableUpdater.hh | 2 +- src/accel/detail/NaviTouchableUpdater.hh | 2 +- src/celeritas/Types.hh | 12 ++++++------ .../alongstep/AlongStepGeneralLinearAction.hh | 2 +- .../alongstep/AlongStepUniformMscAction.hh | 2 +- src/celeritas/em/xs/RBDiffXsCalculator.hh | 2 +- src/celeritas/ext/GeantImporter.hh | 2 +- src/celeritas/ext/RootImporter.hh | 2 +- .../ext/detail/GeantBremsstrahlungProcess.cc | 4 +--- .../ext/detail/GeantBremsstrahlungProcess.hh | 2 +- src/celeritas/global/CoreState.hh | 2 +- src/celeritas/global/Stepper.hh | 2 +- src/celeritas/grid/GridIdFinder.hh | 10 +++------- src/celeritas/io/EventReader.hh | 2 ++ src/celeritas/io/EventWriter.hh | 2 ++ src/celeritas/io/ImporterInterface.hh | 1 + src/celeritas/io/RootEventReader.hh | 3 ++- src/celeritas/io/RootEventWriter.hh | 3 ++- src/celeritas/mat/ElementSelector.hh | 6 ++---- src/celeritas/optical/CoreState.hh | 2 +- src/celeritas/optical/Model.hh | 5 +---- src/celeritas/optical/Types.hh | 2 +- src/celeritas/phys/PrimaryGenerator.hh | 3 ++- .../random/distribution/NormalDistribution.hh | 12 +++++++----- src/celeritas/track/SortTracksAction.hh | 2 +- src/celeritas/user/ActionDiagnostic.hh | 2 +- src/celeritas/user/StepDiagnostic.hh | 2 +- src/corecel/Assert.hh | 4 ++-- src/corecel/DeviceRuntimeApi.hh | 2 +- src/corecel/Macros.hh | 5 ++++- src/corecel/cont/EnumArray.hh | 2 ++ src/corecel/cont/InitializedValue.hh | 2 ++ src/corecel/cont/MiniStack.hh | 4 ++-- src/corecel/cont/Span.hh | 12 ++++++------ src/corecel/cont/detail/RangeImpl.hh | 1 + src/corecel/data/AuxStateVec.hh | 2 ++ src/corecel/data/Collection.hh | 6 ++++++ src/corecel/data/CollectionAlgorithms.hh | 2 +- src/corecel/data/CollectionStateStore.hh | 3 +++ src/corecel/data/DeviceVector.hh | 4 ++++ src/corecel/data/LdgIterator.hh | 2 -- src/corecel/data/ParamsDataInterface.hh | 2 ++ src/corecel/data/detail/FillInvalid.hh | 1 + src/corecel/io/Label.hh | 5 ++++- src/corecel/io/StreamableVariant.hh | 3 ++- src/corecel/io/StringEnumMapper.hh | 3 +++ src/corecel/math/Algorithms.hh | 1 + src/corecel/math/HashUtils.hh | 1 + src/corecel/sys/ActionInterface.hh | 4 ++-- src/corecel/sys/MultiExceptionHandler.hh | 6 ++++-- src/corecel/sys/ScopedMpiInit.cc | 2 +- src/corecel/sys/ScopedMpiInit.hh | 2 ++ src/corecel/sys/Stream.hh | 3 +++ src/geocel/g4/GeantGeoParams.hh | 5 ++++- src/geocel/g4/GeantGeoTrackView.hh | 6 +++--- src/geocel/rasterize/RaytraceImager.hh | 3 +++ src/orange/OrangeParams.cc | 1 - src/orange/detail/LevelStateAccessor.hh | 7 +++++++ src/orange/orangeinp/CsgTypes.hh | 5 ++++- src/orange/orangeinp/IntersectRegion.hh | 3 ++- src/orange/orangeinp/PolySolid.cc | 7 +++---- src/orange/orangeinp/PolySolid.hh | 2 +- src/orange/orangeinp/ProtoInterface.hh | 2 +- src/orange/orangeinp/Shape.hh | 3 ++- src/orange/orangeinp/Solid.hh | 3 ++- src/orange/orangeinp/UnitProto.hh | 2 ++ src/orange/orangeinp/detail/VolumeBuilder.hh | 13 ++++++++++--- 78 files changed, 172 insertions(+), 103 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index 44d03f0fc5..c1c5f1b64a 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -9,6 +9,7 @@ Checks: | -performance-avoid-endl -performance-unnecessary-value-param -performance-enum-size + -performance-noexcept-destructor cppcoreguidelines-* -cppcoreguidelines-owning-memory -cppcoreguidelines-narrowing-conversions @@ -25,21 +26,28 @@ Checks: | -cppcoreguidelines-noexcept-swap -cppcoreguidelines-noexcept-move-operations -cppcoreguidelines-noexcept-destructor + -cppcoreguidelines-virtual-class-destructor + -cppcoreguidelines-c-copy-assignment-signature + -cppcoreguidelines-missing-std-forward + -cppcoreguidelines-special-member-functions bugprone-* -bugprone-sizeof-expression -bugprone-narrowing-conversions -bugprone-macro-parentheses -bugprone-easily-swappable-parameters -bugprone-implicit-widening-of-multiplication-result + -bugprone-exception-escape WarningsAsErrors: '*' CheckOptions: cppcoreguidelines-macro-usage.CheckCapsOnly: true cppcoreguidelines-avoid-do-while.IgnoreMacros: true cppcoreguidelines-rvalue-reference-param-not-moved.IgnoreNonDeducedTemplateTypes: true cppcoreguidelines-rvalue-reference-param-not-moved.AllowPartialMove: true + cppcoreguidelines-special-member-functions.AllowSoleDefaultDtor: true cppcoreguidelines-avoid-non-const-global-variables.AllowInternalLinkage: true performance-move-const-arg.CheckTriviallyCopyableMove: false performance-move-const-arg.CheckMoveToConstRef: false -HeaderFilterRegex: '' + +HeaderFilterRegex: '(example|app|src/(accel|celeritas|corecel|orange|geocel))/.*\.hh$' FormatStyle: file ... diff --git a/app/celer-g4/ActionInitialization.cc b/app/celer-g4/ActionInitialization.cc index c3dfeb1f5d..31b3f52761 100644 --- a/app/celer-g4/ActionInitialization.cc +++ b/app/celer-g4/ActionInitialization.cc @@ -29,7 +29,7 @@ namespace app * The parameters will be distributed to worker threads and all the actions. */ ActionInitialization::ActionInitialization(SPParams params) - : params_{std::move(params)}, init_shared_{true} + : params_{std::move(params)} { CELER_EXPECT(params_); diff --git a/app/celer-g4/ActionInitialization.hh b/app/celer-g4/ActionInitialization.hh index 2d43368e1a..4d83dcb4e8 100644 --- a/app/celer-g4/ActionInitialization.hh +++ b/app/celer-g4/ActionInitialization.hh @@ -43,7 +43,7 @@ class ActionInitialization final : public G4VUserActionInitialization SPParams params_; SPDiagnostics diagnostics_; int num_events_{0}; - mutable bool init_shared_; + mutable bool init_shared_{true}; }; //---------------------------------------------------------------------------// diff --git a/app/celer-g4/DetectorConstruction.cc b/app/celer-g4/DetectorConstruction.cc index 7abe6c7af1..be5ea6bce5 100644 --- a/app/celer-g4/DetectorConstruction.cc +++ b/app/celer-g4/DetectorConstruction.cc @@ -312,7 +312,6 @@ void DetectorConstruction::ConstructSDandField() * Apply a function to the range of volumes for each detector. */ template -// NOLINTNEXTLINE(cppcoreguidelines-missing-std-forward) void DetectorConstruction::foreach_detector(F&& apply_to_range) const { auto start = detectors_.begin(); diff --git a/app/celer-g4/RootIO.hh b/app/celer-g4/RootIO.hh index f8a1b1b436..48d717d4ec 100644 --- a/app/celer-g4/RootIO.hh +++ b/app/celer-g4/RootIO.hh @@ -51,13 +51,6 @@ class RootIO private: // Construct by initializing TFile and TTree on each worker thread RootIO(); - RootIO(RootIO&&) = default; - - // Assignment operator - RootIO& operator=(RootIO&&) = default; - - // Default destructor - ~RootIO() = default; //// HELPER FUNCTIONS //// diff --git a/app/celer-g4/SensitiveDetector.cc b/app/celer-g4/SensitiveDetector.cc index cf0cd6767b..b1eb7714a6 100644 --- a/app/celer-g4/SensitiveDetector.cc +++ b/app/celer-g4/SensitiveDetector.cc @@ -30,7 +30,7 @@ namespace app * Construct with sensitive detector name. */ SensitiveDetector::SensitiveDetector(std::string name) - : G4VSensitiveDetector(name), hcid_{-1}, collection_{nullptr} + : G4VSensitiveDetector(name) { this->collectionName.insert(name); } diff --git a/app/celer-g4/SensitiveDetector.hh b/app/celer-g4/SensitiveDetector.hh index 9d8c181fbd..bc2b4761a9 100644 --- a/app/celer-g4/SensitiveDetector.hh +++ b/app/celer-g4/SensitiveDetector.hh @@ -41,8 +41,8 @@ class SensitiveDetector final : public G4VSensitiveDetector bool ProcessHits(G4Step*, G4TouchableHistory*) final; private: - int hcid_; - SensitiveHitsCollection* collection_; + int hcid_{-1}; + SensitiveHitsCollection* collection_{nullptr}; }; //---------------------------------------------------------------------------// diff --git a/src/accel/HepMC3PrimaryGenerator.hh b/src/accel/HepMC3PrimaryGenerator.hh index cd20f23d61..829991224c 100644 --- a/src/accel/HepMC3PrimaryGenerator.hh +++ b/src/accel/HepMC3PrimaryGenerator.hh @@ -51,6 +51,7 @@ class HepMC3PrimaryGenerator final : public G4VPrimaryGenerator explicit HepMC3PrimaryGenerator(std::string const& filename); CELER_DELETE_COPY_MOVE(HepMC3PrimaryGenerator); + ~HepMC3PrimaryGenerator() final = default; //! Add primaries to Geant4 event void GeneratePrimaryVertex(G4Event* g4_event) final; diff --git a/src/accel/RZMapMagneticField.hh b/src/accel/RZMapMagneticField.hh index 3b88fa62bc..b808789604 100644 --- a/src/accel/RZMapMagneticField.hh +++ b/src/accel/RZMapMagneticField.hh @@ -38,7 +38,8 @@ class RZMapMagneticField : public G4MagneticField inline explicit RZMapMagneticField(SPConstFieldParams field_params); // Calculate values of the magnetic field vector - inline void GetFieldValue(double const point[3], double* field) const; + inline void + GetFieldValue(double const point[3], double* field) const override; private: SPConstFieldParams params_; diff --git a/src/accel/SetupOptionsMessenger.hh b/src/accel/SetupOptionsMessenger.hh index e6e40e0645..79931b0c05 100644 --- a/src/accel/SetupOptionsMessenger.hh +++ b/src/accel/SetupOptionsMessenger.hh @@ -64,7 +64,7 @@ class SetupOptionsMessenger : public G4UImessenger explicit SetupOptionsMessenger(SetupOptions* options); // Default destructor - ~SetupOptionsMessenger(); + ~SetupOptionsMessenger() override; protected: void SetNewValue(G4UIcommand* command, G4String newValue) override; diff --git a/src/accel/detail/HitProcessor.hh b/src/accel/detail/HitProcessor.hh index f5957ded46..8a4be8c8a8 100644 --- a/src/accel/detail/HitProcessor.hh +++ b/src/accel/detail/HitProcessor.hh @@ -12,6 +12,7 @@ #include #include +#include "corecel/Macros.hh" #include "celeritas/Types.hh" #include "celeritas/geo/GeoFwd.hh" #include "celeritas/user/DetectorSteps.hh" @@ -81,6 +82,7 @@ class HitProcessor // Log on destruction ~HitProcessor(); + CELER_DEFAULT_MOVE_DELETE_COPY(HitProcessor); // Process CPU-generated hits void operator()(StepStateHostRef const&); diff --git a/src/accel/detail/LevelTouchableUpdater.hh b/src/accel/detail/LevelTouchableUpdater.hh index 10449962e4..5ce3f79a2b 100644 --- a/src/accel/detail/LevelTouchableUpdater.hh +++ b/src/accel/detail/LevelTouchableUpdater.hh @@ -47,7 +47,7 @@ class LevelTouchableUpdater final : public TouchableUpdaterInterface explicit LevelTouchableUpdater(SPConstGeo); // Destroy pointers - ~LevelTouchableUpdater(); + ~LevelTouchableUpdater() final; // Update from a particular detector step bool operator()(DetectorStepOutput const& out, diff --git a/src/accel/detail/NaviTouchableUpdater.hh b/src/accel/detail/NaviTouchableUpdater.hh index b830d05984..058ea96c1a 100644 --- a/src/accel/detail/NaviTouchableUpdater.hh +++ b/src/accel/detail/NaviTouchableUpdater.hh @@ -56,7 +56,7 @@ class NaviTouchableUpdater final : public TouchableUpdaterInterface G4VPhysicalVolume const* world); // Default external deleter - ~NaviTouchableUpdater(); + ~NaviTouchableUpdater() final; // Update from a particular detector step bool operator()(DetectorStepOutput const& out, diff --git a/src/celeritas/Types.hh b/src/celeritas/Types.hh index f73bb5d950..2a83000c9a 100644 --- a/src/celeritas/Types.hh +++ b/src/celeritas/Types.hh @@ -40,10 +40,10 @@ using IsotopeId = OpaqueId; //! Opaque index of a material modified by physics options // TODO: rename to PhysMatId; equivalent to "material cuts couple" -using MaterialId = OpaqueId; +using MaterialId = OpaqueId; //! Opaque index of model in the list of physics processes -using ModelId = OpaqueId; +using ModelId = OpaqueId; //! Opaque index to a material with optical properties using OpticalMaterialId = OpaqueId; @@ -52,7 +52,7 @@ using OpticalMaterialId = OpaqueId; using ParticleId = OpaqueId; //! Opaque index of physics process -using ProcessId = OpaqueId; +using ProcessId = OpaqueId; //! Unique ID (for an event) of a track among all primaries and secondaries using TrackId = OpaqueId; @@ -61,6 +61,9 @@ using TrackId = OpaqueId; // (detailed type aliases) //---------------------------------------------------------------------------// +//! Opaque index of particle-nucleon cascade channel +using ChannelId = OpaqueId; + //! Opaque index for mapping volume-specific "sensitive detector" objects using DetectorId = OpaqueId; @@ -79,9 +82,6 @@ using ParticleModelId = OpaqueId; //! Opaque index of electron subshell using SubshellId = OpaqueId; -//! Opaque index of particle-nucleon cascade channel -using ChannelId = OpaqueId; - //---------------------------------------------------------------------------// // ENUMERATIONS //---------------------------------------------------------------------------// diff --git a/src/celeritas/alongstep/AlongStepGeneralLinearAction.hh b/src/celeritas/alongstep/AlongStepGeneralLinearAction.hh index 54b9e27366..3bc0f3234c 100644 --- a/src/celeritas/alongstep/AlongStepGeneralLinearAction.hh +++ b/src/celeritas/alongstep/AlongStepGeneralLinearAction.hh @@ -56,7 +56,7 @@ class AlongStepGeneralLinearAction final : public CoreStepActionInterface SPConstMsc msc); // Default destructor - ~AlongStepGeneralLinearAction(); + ~AlongStepGeneralLinearAction() final; // Launch kernel with host data void step(CoreParams const&, CoreStateHost&) const final; diff --git a/src/celeritas/alongstep/AlongStepUniformMscAction.hh b/src/celeritas/alongstep/AlongStepUniformMscAction.hh index 6ce1f097e3..1b272276e8 100644 --- a/src/celeritas/alongstep/AlongStepUniformMscAction.hh +++ b/src/celeritas/alongstep/AlongStepUniformMscAction.hh @@ -55,7 +55,7 @@ class AlongStepUniformMscAction final : public CoreStepActionInterface SPConstMsc msc); // Default destructor - ~AlongStepUniformMscAction(); + ~AlongStepUniformMscAction() final; // Launch kernel with host data void step(CoreParams const&, CoreStateHost&) const final; diff --git a/src/celeritas/em/xs/RBDiffXsCalculator.hh b/src/celeritas/em/xs/RBDiffXsCalculator.hh index 18a19ebda9..3d5c0a1725 100644 --- a/src/celeritas/em/xs/RBDiffXsCalculator.hh +++ b/src/celeritas/em/xs/RBDiffXsCalculator.hh @@ -123,6 +123,7 @@ RBDiffXsCalculator::RBDiffXsCalculator(RelativisticBremRef const& shared, , material_(material) , element_(material.make_element_view(elcomp_id)) , total_energy_(value_as(particle.total_energy())) + , dielectric_suppression_(shared.dielectric_suppression()) { real_type density_factor = material.electron_density() * detail::migdal_constant(); @@ -133,7 +134,6 @@ RBDiffXsCalculator::RBDiffXsCalculator(RelativisticBremRef const& shared, * value_as(detail::lpm_constant()); real_type lpm_threshold = lpm_energy * std::sqrt(density_factor); enable_lpm_ = (shared.enable_lpm && (total_energy_ > lpm_threshold)); - dielectric_suppression_ = shared.dielectric_suppression(); } //---------------------------------------------------------------------------// diff --git a/src/celeritas/ext/GeantImporter.hh b/src/celeritas/ext/GeantImporter.hh index 173833e663..f9af815063 100644 --- a/src/celeritas/ext/GeantImporter.hh +++ b/src/celeritas/ext/GeantImporter.hh @@ -90,7 +90,7 @@ class GeantImporter final : public ImporterInterface ImportData operator()(DataSelection const& selection); //! Fill all available data from Geant4 - ImportData operator()() { return (*this)(DataSelection{}); } + ImportData operator()() final { return (*this)(DataSelection{}); } private: // Optional setup if celeritas handles initialization diff --git a/src/celeritas/ext/RootImporter.hh b/src/celeritas/ext/RootImporter.hh index 8611d0c34f..f3f355d917 100644 --- a/src/celeritas/ext/RootImporter.hh +++ b/src/celeritas/ext/RootImporter.hh @@ -58,7 +58,7 @@ class RootImporter final : public ImporterInterface } // Load data from the ROOT files - ImportData operator()(); + ImportData operator()() final; private: // ROOT file diff --git a/src/celeritas/ext/detail/GeantBremsstrahlungProcess.cc b/src/celeritas/ext/detail/GeantBremsstrahlungProcess.cc index a2aa2708b4..52162d8021 100644 --- a/src/celeritas/ext/detail/GeantBremsstrahlungProcess.cc +++ b/src/celeritas/ext/detail/GeantBremsstrahlungProcess.cc @@ -39,9 +39,7 @@ namespace detail * Construct with model selection. */ GeantBremsstrahlungProcess::GeantBremsstrahlungProcess(ModelSelection selection) - : G4VEnergyLossProcess("eBrem") - , is_initialized_(false) - , model_selection_(selection) + : G4VEnergyLossProcess("eBrem"), model_selection_(selection) { CELER_VALIDATE(selection != ModelSelection::none, << "Cannot initialize GeantBremsstrahlungProcess with " diff --git a/src/celeritas/ext/detail/GeantBremsstrahlungProcess.hh b/src/celeritas/ext/detail/GeantBremsstrahlungProcess.hh index f0ca680d35..9cfdceb3f4 100644 --- a/src/celeritas/ext/detail/GeantBremsstrahlungProcess.hh +++ b/src/celeritas/ext/detail/GeantBremsstrahlungProcess.hh @@ -51,7 +51,7 @@ class GeantBremsstrahlungProcess : public G4VEnergyLossProcess void StreamProcessInfo(std::ostream& output) const override; private: - bool is_initialized_; + bool is_initialized_{false}; ModelSelection model_selection_; }; diff --git a/src/celeritas/global/CoreState.hh b/src/celeritas/global/CoreState.hh index 7dd9224aac..e55673be3c 100644 --- a/src/celeritas/global/CoreState.hh +++ b/src/celeritas/global/CoreState.hh @@ -90,7 +90,7 @@ class CoreState final : public CoreStateInterface size_type num_track_slots); // Default destructor - ~CoreState(); + ~CoreState() final; // Prevent move/copy CELER_DELETE_COPY_MOVE(CoreState); diff --git a/src/celeritas/global/Stepper.hh b/src/celeritas/global/Stepper.hh index 8760773aab..502c7e71c3 100644 --- a/src/celeritas/global/Stepper.hh +++ b/src/celeritas/global/Stepper.hh @@ -157,7 +157,7 @@ class Stepper final : public StepperInterface explicit Stepper(Input input); // Default destructor - ~Stepper(); + ~Stepper() final; // Warm up before stepping void warm_up() final; diff --git a/src/celeritas/grid/GridIdFinder.hh b/src/celeritas/grid/GridIdFinder.hh index 275fb42a99..0a2027141a 100644 --- a/src/celeritas/grid/GridIdFinder.hh +++ b/src/celeritas/grid/GridIdFinder.hh @@ -86,14 +86,10 @@ GridIdFinder::operator()(argument_type quant) const -> result_type { auto iter = celeritas::lower_bound(grid_.begin(), grid_.end(), quant.value()); - if (iter == grid_.end()) + if (iter == grid_.end() + || (iter == grid_.begin() && quant.value() != *iter)) { - // Higher than end point - return {}; - } - else if (iter == grid_.begin() && quant.value() != *iter) - { - // Below first point + // Higher than end point or below first point return {}; } else if (iter + 1 == grid_.end() || quant.value() != *iter) diff --git a/src/celeritas/io/EventReader.hh b/src/celeritas/io/EventReader.hh index 77a4b2cc9f..e7a5ef0088 100644 --- a/src/celeritas/io/EventReader.hh +++ b/src/celeritas/io/EventReader.hh @@ -56,6 +56,8 @@ class EventReader : public EventReaderInterface //! Prevent copying and moving CELER_DELETE_COPY_MOVE(EventReader); + ~EventReader() override = default; + // Read a single event from the event record result_type operator()() final; diff --git a/src/celeritas/io/EventWriter.hh b/src/celeritas/io/EventWriter.hh index b17b36e56f..caab5d65f6 100644 --- a/src/celeritas/io/EventWriter.hh +++ b/src/celeritas/io/EventWriter.hh @@ -61,6 +61,8 @@ class EventWriter : public EventWriterInterface //! Prevent copying and moving due to file ownership CELER_DELETE_COPY_MOVE(EventWriter); + ~EventWriter() override = default; + // Write all the primaries from a single event void operator()(VecPrimary const& primaries) final; diff --git a/src/celeritas/io/ImporterInterface.hh b/src/celeritas/io/ImporterInterface.hh index bb17d683df..5f5aed92c5 100644 --- a/src/celeritas/io/ImporterInterface.hh +++ b/src/celeritas/io/ImporterInterface.hh @@ -24,6 +24,7 @@ class ImporterInterface protected: ImporterInterface() = default; CELER_DEFAULT_COPY_MOVE(ImporterInterface); + ~ImporterInterface() = default; }; //---------------------------------------------------------------------------// diff --git a/src/celeritas/io/RootEventReader.hh b/src/celeritas/io/RootEventReader.hh index 9c4bd31d96..4b530a2a38 100644 --- a/src/celeritas/io/RootEventReader.hh +++ b/src/celeritas/io/RootEventReader.hh @@ -47,6 +47,7 @@ class RootEventReader : public EventReaderInterface //! Prevent copying and moving CELER_DELETE_COPY_MOVE(RootEventReader); + ~RootEventReader() override = default; // Read a user-defined event from the ROOT file result_type operator()(EventId event_id); @@ -86,7 +87,7 @@ inline RootEventReader::RootEventReader(std::string const&, SPConstParticles) CELER_DISCARD(num_events_); CELER_DISCARD(entry_count_); CELER_DISCARD(expected_event_id_); - CELER_DISCARD(event_to_entry_); + CELER_DISCARD(event_to_entry_); // NOLINT(bugprone-sizeof-container) CELER_NOT_CONFIGURED("ROOT"); } diff --git a/src/celeritas/io/RootEventWriter.hh b/src/celeritas/io/RootEventWriter.hh index 30e4690ff2..2e9f7ce892 100644 --- a/src/celeritas/io/RootEventWriter.hh +++ b/src/celeritas/io/RootEventWriter.hh @@ -40,9 +40,10 @@ class RootEventWriter : public EventWriterInterface //! Prevent copying and moving CELER_DELETE_COPY_MOVE(RootEventWriter); + ~RootEventWriter() override = default; // Export primaries to ROOT - void operator()(VecPrimary const& primaries); + void operator()(VecPrimary const& primaries) override; private: //// DATA //// diff --git a/src/celeritas/mat/ElementSelector.hh b/src/celeritas/mat/ElementSelector.hh index e8ebf527d7..a1dee7d6b1 100644 --- a/src/celeritas/mat/ElementSelector.hh +++ b/src/celeritas/mat/ElementSelector.hh @@ -82,7 +82,7 @@ class ElementSelector private: Span elements_; - real_type material_xs_; + real_type material_xs_{0}; real_type* elemental_xs_; }; @@ -96,9 +96,7 @@ template CELER_FUNCTION ElementSelector::ElementSelector(MaterialView const& material, MicroXsCalc&& calc_micro_xs, SpanReal storage) - : elements_(material.elements()) - , material_xs_(0) - , elemental_xs_(storage.data()) + : elements_(material.elements()), elemental_xs_(storage.data()) { CELER_EXPECT(!elements_.empty()); CELER_EXPECT(storage.size() >= material.num_elements()); diff --git a/src/celeritas/optical/CoreState.hh b/src/celeritas/optical/CoreState.hh index 862f41921d..6d7b956601 100644 --- a/src/celeritas/optical/CoreState.hh +++ b/src/celeritas/optical/CoreState.hh @@ -40,7 +40,7 @@ class CoreStateInterface : public AuxStateInterface public: // Support polymorphic deletion - virtual ~CoreStateInterface(); + ~CoreStateInterface() override; //! Thread/stream ID virtual StreamId stream_id() const = 0; diff --git a/src/celeritas/optical/Model.hh b/src/celeritas/optical/Model.hh index d084dd2300..369431ed2e 100644 --- a/src/celeritas/optical/Model.hh +++ b/src/celeritas/optical/Model.hh @@ -32,10 +32,7 @@ class Model : public OpticalStepActionInterface, public ConcreteAction using ConcreteAction::ConcreteAction; //! Action order for optical models is always post-step - StepActionOrder order() const override final - { - return StepActionOrder::post; - } + StepActionOrder order() const override { return StepActionOrder::post; } //! Build mean free path grids for all optical materials virtual void build_mfps(OpticalMaterialId mat, MfpBuilder& build) const = 0; diff --git a/src/celeritas/optical/Types.hh b/src/celeritas/optical/Types.hh index 1ce15dc7a9..197b48a687 100644 --- a/src/celeritas/optical/Types.hh +++ b/src/celeritas/optical/Types.hh @@ -19,7 +19,7 @@ namespace celeritas using ScintillationParticleId = OpaqueId; //! Opaque index to a scintillation spectrum -using ParticleScintSpectrumId = OpaqueId; +using ParticleScintSpectrumId = OpaqueId; //---------------------------------------------------------------------------// /*! diff --git a/src/celeritas/phys/PrimaryGenerator.hh b/src/celeritas/phys/PrimaryGenerator.hh index 1710b6cc0c..43c07bf282 100644 --- a/src/celeritas/phys/PrimaryGenerator.hh +++ b/src/celeritas/phys/PrimaryGenerator.hh @@ -66,12 +66,13 @@ class PrimaryGenerator : public EventReaderInterface //! Prevent copying and moving CELER_DELETE_COPY_MOVE(PrimaryGenerator); + ~PrimaryGenerator() override = default; // Generate primary particles from a single event result_type operator()() final; //! Get total number of events - size_type num_events() const { return num_events_; } + size_type num_events() const override { return num_events_; } private: size_type num_events_{}; diff --git a/src/celeritas/random/distribution/NormalDistribution.hh b/src/celeritas/random/distribution/NormalDistribution.hh index 0b345e9ed4..1e826ecda3 100644 --- a/src/celeritas/random/distribution/NormalDistribution.hh +++ b/src/celeritas/random/distribution/NormalDistribution.hh @@ -62,14 +62,16 @@ class NormalDistribution inline CELER_FUNCTION NormalDistribution(NormalDistribution const& other); // Reset spare value of other distribution - inline CELER_FUNCTION NormalDistribution(NormalDistribution&& other); + inline CELER_FUNCTION + NormalDistribution(NormalDistribution&& other) noexcept; // Keep spare value but change distribution inline CELER_FUNCTION NormalDistribution& operator=(NormalDistribution const&); // Possibly use spare value, change distribution - inline CELER_FUNCTION NormalDistribution& operator=(NormalDistribution&&); + inline CELER_FUNCTION NormalDistribution& + operator=(NormalDistribution&&) noexcept; // Default destructor (rule of 5) ~NormalDistribution() = default; @@ -119,8 +121,8 @@ NormalDistribution::NormalDistribution(NormalDistribution const& other * Reset spare value of other distribution. */ template -CELER_FUNCTION -NormalDistribution::NormalDistribution(NormalDistribution&& other) +CELER_FUNCTION NormalDistribution::NormalDistribution( + NormalDistribution&& other) noexcept : mean_{other.mean_} , stddev_{other.stddev_} , spare_{other.spare_} @@ -148,7 +150,7 @@ NormalDistribution::operator=(NormalDistribution const& other) */ template CELER_FUNCTION NormalDistribution& -NormalDistribution::operator=(NormalDistribution&& other) +NormalDistribution::operator=(NormalDistribution&& other) noexcept { mean_ = other.mean_; stddev_ = other.stddev_; diff --git a/src/celeritas/track/SortTracksAction.hh b/src/celeritas/track/SortTracksAction.hh index 5f62a21bae..e6140ee60d 100644 --- a/src/celeritas/track/SortTracksAction.hh +++ b/src/celeritas/track/SortTracksAction.hh @@ -29,7 +29,7 @@ class SortTracksAction final : public CoreStepActionInterface, SortTracksAction(ActionId id, TrackOrder track_order); //! Default destructor - ~SortTracksAction() = default; + ~SortTracksAction() final = default; //! Execute the action with host data void step(CoreParams const& params, CoreStateHost& state) const final; diff --git a/src/celeritas/user/ActionDiagnostic.hh b/src/celeritas/user/ActionDiagnostic.hh index a3c0996bb6..6c83cf6b05 100644 --- a/src/celeritas/user/ActionDiagnostic.hh +++ b/src/celeritas/user/ActionDiagnostic.hh @@ -63,7 +63,7 @@ class ActionDiagnostic final : public CoreStepActionInterface, explicit ActionDiagnostic(ActionId id); // Default destructor - ~ActionDiagnostic(); + ~ActionDiagnostic() final; //!@{ //! \name Action interface diff --git a/src/celeritas/user/StepDiagnostic.hh b/src/celeritas/user/StepDiagnostic.hh index 505bedf827..a9650178bc 100644 --- a/src/celeritas/user/StepDiagnostic.hh +++ b/src/celeritas/user/StepDiagnostic.hh @@ -52,7 +52,7 @@ class StepDiagnostic final : public CoreStepActionInterface, size_type num_streams); //! Default destructor - ~StepDiagnostic(); + ~StepDiagnostic() final; //!@{ //! \name ExplicitAction interface diff --git a/src/corecel/Assert.hh b/src/corecel/Assert.hh index 565cc4a825..37b4688743 100644 --- a/src/corecel/Assert.hh +++ b/src/corecel/Assert.hh @@ -464,7 +464,7 @@ class DebugError : public std::logic_error CELER_DEFAULT_COPY_MOVE(DebugError); // Default destructor to anchor vtable - ~DebugError(); + ~DebugError() override; //! Access the debug data DebugErrorDetails const& details() const { return details_; } @@ -485,7 +485,7 @@ class RuntimeError : public std::runtime_error CELER_DEFAULT_COPY_MOVE(RuntimeError); // Default destructor to anchor vtable - ~RuntimeError(); + ~RuntimeError() override; //! Access detailed information RuntimeErrorDetails const& details() const { return details_; } diff --git a/src/corecel/DeviceRuntimeApi.hh b/src/corecel/DeviceRuntimeApi.hh index 27a2b68c00..2186e136ff 100644 --- a/src/corecel/DeviceRuntimeApi.hh +++ b/src/corecel/DeviceRuntimeApi.hh @@ -61,4 +61,4 @@ * (Unfortunately, since the use of this symbol is embedded in a macro, IWYU * won't include this file automatically.) */ -extern int CorecelDeviceRuntimeApiHh; +extern int const CorecelDeviceRuntimeApiHh; diff --git a/src/corecel/Macros.hh b/src/corecel/Macros.hh index c720b06ef4..872e230727 100644 --- a/src/corecel/Macros.hh +++ b/src/corecel/Macros.hh @@ -42,11 +42,12 @@ # define CELER_FORCEINLINE inline #endif +// NOLINTBEGIN(cppcoreguidelines-macro-to-enum) //! Detection for the current compiler isn't supported yet #define CELER_COMPILER_UNKNOWN 0 //! Compiling with clang, or a clang-based compiler defining __clang__ (hipcc) #define CELER_COMPILER_CLANG 1 - +// NOLINTEND(cppcoreguidelines-macro-to-enum) /*! * \def CELER_COMPILER * @@ -125,6 +126,7 @@ # define CELER_UNREACHABLE #endif +// NOLINTBEGIN(cppcoreguidelines-macro-to-enum) /*! * \def CELER_USE_DEVICE * @@ -135,6 +137,7 @@ #else # define CELER_USE_DEVICE 0 #endif +// NOLINTEND(cppcoreguidelines-macro-to-enum) /*! * \def CELER_DEVICE_SOURCE diff --git a/src/corecel/cont/EnumArray.hh b/src/corecel/cont/EnumArray.hh index 8d1130d818..af26f7695f 100644 --- a/src/corecel/cont/EnumArray.hh +++ b/src/corecel/cont/EnumArray.hh @@ -27,6 +27,8 @@ namespace celeritas * \todo The template parameters are reversed!!! */ template +// TODO Remove in clang-tidy-18 +// NOLINTNEXTLINE(bugprone-reserved-identifier) struct EnumArray { static_assert(std::is_enum::value, "Template parameter must be an enum"); diff --git a/src/corecel/cont/InitializedValue.hh b/src/corecel/cont/InitializedValue.hh index 8e6cd9ce5c..da14a624db 100644 --- a/src/corecel/cont/InitializedValue.hh +++ b/src/corecel/cont/InitializedValue.hh @@ -74,6 +74,8 @@ class InitializedValue { } + ~InitializedValue() = default; + //!@} //!@{ //! \name Assignment diff --git a/src/corecel/cont/MiniStack.hh b/src/corecel/cont/MiniStack.hh index e7e238db64..21f69b7d74 100644 --- a/src/corecel/cont/MiniStack.hh +++ b/src/corecel/cont/MiniStack.hh @@ -29,7 +29,7 @@ class MiniStack public: //! Construct with underlying storage. CELER_FUNCTION explicit MiniStack(Span storage) - : data_(storage.data()), size_(0), capacity_(storage.size()) + : data_(storage.data()), capacity_(storage.size()) { } @@ -58,7 +58,7 @@ class MiniStack private: T* data_; - size_type size_; + size_type size_{0}; size_type capacity_; }; diff --git a/src/corecel/cont/Span.hh b/src/corecel/cont/Span.hh index eec4512fc6..b9a06dd393 100644 --- a/src/corecel/cont/Span.hh +++ b/src/corecel/cont/Span.hh @@ -10,6 +10,8 @@ #include #include +#include "corecel/Macros.hh" + #include "Array.hh" #include "detail/SpanImpl.hh" @@ -95,12 +97,10 @@ class Span : s_(other.data(), other.size()) { } - - //! Copy constructor (same template parameters) - Span(Span const&) noexcept = default; - - //! Assignment (same template parameters) - Span& operator=(Span const&) noexcept = default; + // TODO Remove in clang-tidy-18 + // NOLINTNEXTLINE(performance-noexcept-move-constructor) + CELER_DEFAULT_COPY_MOVE(Span); + ~Span() = default; //// ACCESS //// diff --git a/src/corecel/cont/detail/RangeImpl.hh b/src/corecel/cont/detail/RangeImpl.hh index a96ba063e7..c1ac633fa9 100644 --- a/src/corecel/cont/detail/RangeImpl.hh +++ b/src/corecel/cont/detail/RangeImpl.hh @@ -240,6 +240,7 @@ class range_iter CELER_CONSTEXPR_FUNCTION value_type value() const { return value_; } protected: + // NOLINTNEXTLINE(cppcoreguidelines-non-private-member-variables-in-classes) value_type value_; }; diff --git a/src/corecel/data/AuxStateVec.hh b/src/corecel/data/AuxStateVec.hh index 8dd290b82c..96af12ac1a 100644 --- a/src/corecel/data/AuxStateVec.hh +++ b/src/corecel/data/AuxStateVec.hh @@ -55,6 +55,8 @@ class AuxStateVec // Allow moving; copying is prohibited due to unique pointers CELER_DEFAULT_MOVE_DELETE_COPY(AuxStateVec); + ~AuxStateVec() = default; + // Access auxiliary state interfaces inline AuxStateInterface& at(AuxId); inline AuxStateInterface const& at(AuxId) const; diff --git a/src/corecel/data/Collection.hh b/src/corecel/data/Collection.hh index 4da2aa758b..d77d033bad 100644 --- a/src/corecel/data/Collection.hh +++ b/src/corecel/data/Collection.hh @@ -277,9 +277,13 @@ class Collection //! Default constructors Collection() = default; Collection(Collection const&) = default; + // TODO Remove in clang-tidy-18 + // NOLINTNEXTLINE(performance-noexcept-move-constructor) Collection(Collection&&) = default; //!@} + ~Collection() = default; + // Construct from another collection template explicit inline Collection(Collection const& other); @@ -291,6 +295,8 @@ class Collection //!@{ //! Default assignment Collection& operator=(Collection const& other) = default; + // TODO Remove in clang-tidy-18 + // NOLINTNEXTLINE(performance-noexcept-move-constructor) Collection& operator=(Collection&& other) = default; //!@} diff --git a/src/corecel/data/CollectionAlgorithms.hh b/src/corecel/data/CollectionAlgorithms.hh index 2b740a6b91..8f57dd8ecf 100644 --- a/src/corecel/data/CollectionAlgorithms.hh +++ b/src/corecel/data/CollectionAlgorithms.hh @@ -29,7 +29,7 @@ void fill(T&& value, Collection* col) static_assert(W != Ownership::const_reference, "const references cannot be filled"); CELER_EXPECT(col); - Filler fill_impl{value}; + Filler fill_impl{std::forward(value)}; fill_impl((*col)[AllItems{}]); } diff --git a/src/corecel/data/CollectionStateStore.hh b/src/corecel/data/CollectionStateStore.hh index c9ec5189a9..eba1720431 100644 --- a/src/corecel/data/CollectionStateStore.hh +++ b/src/corecel/data/CollectionStateStore.hh @@ -51,6 +51,7 @@ class CollectionStateStore public: CollectionStateStore() = default; + ~CollectionStateStore() = default; // Construct from parameters and stream ID template class P> @@ -77,6 +78,8 @@ class CollectionStateStore inline CollectionStateStore& operator=(S const& other); //! Default move, delete copy (since ref "points to" val) + // TODO Remove in clang-tidy-18 + // NOLINTNEXTLINE(performance-noexcept-move-constructor) CELER_DEFAULT_MOVE_DELETE_COPY(CollectionStateStore); //! Whether any data is being stored diff --git a/src/corecel/data/DeviceVector.hh b/src/corecel/data/DeviceVector.hh index dc700c50af..ea67a9e17a 100644 --- a/src/corecel/data/DeviceVector.hh +++ b/src/corecel/data/DeviceVector.hh @@ -194,6 +194,7 @@ template void DeviceVector::copy_to_device(SpanConstT data) { CELER_EXPECT(data.size() == this->size()); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) allocation_.copy_to_device({reinterpret_cast(data.data()), data.size() * sizeof(T)}); } @@ -207,6 +208,7 @@ void DeviceVector::copy_to_host(SpanT data) const { CELER_EXPECT(data.size() == this->size()); allocation_.copy_to_host( + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) {reinterpret_cast(data.data()), data.size() * sizeof(T)}); } @@ -217,6 +219,7 @@ void DeviceVector::copy_to_host(SpanT data) const template T* DeviceVector::data() { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) return reinterpret_cast(allocation_.device_ref().data()); } @@ -227,6 +230,7 @@ T* DeviceVector::data() template T const* DeviceVector::data() const { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) return reinterpret_cast(allocation_.device_ref().data()); } diff --git a/src/corecel/data/LdgIterator.hh b/src/corecel/data/LdgIterator.hh index 4ee0de26fe..daee3af129 100644 --- a/src/corecel/data/LdgIterator.hh +++ b/src/corecel/data/LdgIterator.hh @@ -51,7 +51,6 @@ class LdgIterator //!@{ //! Construct a pointer constexpr LdgIterator() noexcept = default; - constexpr LdgIterator(LdgIterator const&) noexcept = default; CELER_CONSTEXPR_FUNCTION LdgIterator(std::nullptr_t) noexcept {} CELER_CONSTEXPR_FUNCTION explicit LdgIterator(pointer ptr) noexcept : ptr_{ptr} @@ -109,7 +108,6 @@ class LdgIterator { return LoadPolicyT::read(ptr_ + n); } - LdgIterator& operator=(LdgIterator const&) = default; //!@} //!@{ diff --git a/src/corecel/data/ParamsDataInterface.hh b/src/corecel/data/ParamsDataInterface.hh index bb9ef1b63c..1b3910d477 100644 --- a/src/corecel/data/ParamsDataInterface.hh +++ b/src/corecel/data/ParamsDataInterface.hh @@ -45,6 +45,8 @@ class ParamsDataInterface // Prohibit copy/move beween interface classes ParamsDataInterface() = default; + // TODO Remove in clang-tidy-18 + // NOLINTNEXTLINE(performance-noexcept-move-constructor) CELER_DEFAULT_COPY_MOVE(ParamsDataInterface); }; diff --git a/src/corecel/data/detail/FillInvalid.hh b/src/corecel/data/detail/FillInvalid.hh index cc50d93edf..19dd247a47 100644 --- a/src/corecel/data/detail/FillInvalid.hh +++ b/src/corecel/data/detail/FillInvalid.hh @@ -85,6 +85,7 @@ struct InvalidValueTraits // all our datatypes. Reinterpret the data as bytes and assign garbage // values. T result; + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) std::memset(reinterpret_cast(&result), 0xd0, sizeof(T)); return result; } diff --git a/src/corecel/io/Label.hh b/src/corecel/io/Label.hh index 1fcbbb8989..c9adb149b6 100644 --- a/src/corecel/io/Label.hh +++ b/src/corecel/io/Label.hh @@ -13,6 +13,8 @@ #include #include +#include "corecel/Config.hh" + #include "corecel/math/HashUtils.hh" namespace celeritas @@ -120,7 +122,8 @@ struct hash { using argument_type = celeritas::Label; using result_type = std::size_t; - result_type operator()(argument_type const& label) const noexcept + result_type operator()(argument_type const& label) const + noexcept(!CELERITAS_DEBUG) { return celeritas::hash_combine(label.name, label.ext); } diff --git a/src/corecel/io/StreamableVariant.hh b/src/corecel/io/StreamableVariant.hh index c8fa30a225..50dd42ba04 100644 --- a/src/corecel/io/StreamableVariant.hh +++ b/src/corecel/io/StreamableVariant.hh @@ -9,6 +9,7 @@ #include #include +#include #include #include "corecel/Assert.hh" @@ -47,7 +48,7 @@ struct GenericToStream template void operator()(T&& obj) const { - this->os << obj; + this->os << std::forward(obj); } }; } // namespace detail diff --git a/src/corecel/io/StringEnumMapper.hh b/src/corecel/io/StringEnumMapper.hh index 870b5e6881..51d83c0a76 100644 --- a/src/corecel/io/StringEnumMapper.hh +++ b/src/corecel/io/StringEnumMapper.hh @@ -57,6 +57,7 @@ class StringEnumMapper // Construct with a function that takes an enum and returns a stringlike template + // NOLINTNEXTLINE(bugprone-forwarding-reference-overload) explicit inline StringEnumMapper(U&& enum_to_string, char const* desc = nullptr); @@ -90,6 +91,8 @@ StringEnumMapper::from_cstring_func(EnumCStringFuncPtr fp, char const* desc) */ template template +// TODO Remove in clang-tidy-18 +// NOLINTNEXTLINE(bugprone-forwarding-reference-overload) StringEnumMapper::StringEnumMapper(U&& enum_to_string, char const* desc) : description_(desc) { diff --git a/src/corecel/math/Algorithms.hh b/src/corecel/math/Algorithms.hh index 0f7edc6e97..4a3cc1c4dd 100644 --- a/src/corecel/math/Algorithms.hh +++ b/src/corecel/math/Algorithms.hh @@ -39,6 +39,7 @@ forward(typename std::remove_reference::type& v) noexcept //! \cond (CELERITAS_DOC_DEV) template CELER_CONSTEXPR_FUNCTION T&& +// NOLINTNEXTLINE(cppcoreguidelines-rvalue-reference-param-not-moved) forward(typename std::remove_reference::type&& v) noexcept { return static_cast(v); diff --git a/src/corecel/math/HashUtils.hh b/src/corecel/math/HashUtils.hh index 2bbfe26239..cc4957bf81 100644 --- a/src/corecel/math/HashUtils.hh +++ b/src/corecel/math/HashUtils.hh @@ -38,6 +38,7 @@ std::size_t hash_as_bytes(Span s) { std::size_t result{}; Hasher hash{&result}; + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) hash(Span{reinterpret_cast(s.data()), s.size() * sizeof(T)}); return result; diff --git a/src/corecel/sys/ActionInterface.hh b/src/corecel/sys/ActionInterface.hh index 03c4cc5a27..aace53de03 100644 --- a/src/corecel/sys/ActionInterface.hh +++ b/src/corecel/sys/ActionInterface.hh @@ -230,7 +230,7 @@ class ConcreteAction : virtual public ActionInterface std::string description) noexcept(!CELERITAS_DEBUG); // Default destructor - ~ConcreteAction() noexcept; + ~ConcreteAction() noexcept override; CELER_DELETE_COPY_MOVE(ConcreteAction); //! ID of this action for verification @@ -308,7 +308,7 @@ class StaticConcreteAction : virtual public ActionInterface std::string_view description) noexcept(!CELERITAS_DEBUG); // Default destructor - ~StaticConcreteAction() = default; + ~StaticConcreteAction() override = default; CELER_DELETE_COPY_MOVE(StaticConcreteAction); //! ID of this action for verification diff --git a/src/corecel/sys/MultiExceptionHandler.hh b/src/corecel/sys/MultiExceptionHandler.hh index 96202455e2..a98aed04de 100644 --- a/src/corecel/sys/MultiExceptionHandler.hh +++ b/src/corecel/sys/MultiExceptionHandler.hh @@ -11,6 +11,8 @@ #include #include +#include "corecel/Config.hh" + #include "corecel/Macros.hh" namespace celeritas @@ -53,7 +55,7 @@ class MultiExceptionHandler CELER_DEFAULT_COPY_MOVE(MultiExceptionHandler); // Terminate if destroyed without handling exceptions - inline ~MultiExceptionHandler(); + inline ~MultiExceptionHandler() noexcept(!CELERITAS_DEBUG); // Thread-safe capture of the given exception void operator()(std::exception_ptr p); @@ -99,7 +101,7 @@ inline void log_and_rethrow(MultiExceptionHandler&& exceptions) /*! * Terminate if destroyed without handling exceptions. */ -MultiExceptionHandler::~MultiExceptionHandler() +MultiExceptionHandler::~MultiExceptionHandler() noexcept(!CELERITAS_DEBUG) { if (CELER_UNLIKELY(!exceptions_.empty())) { diff --git a/src/corecel/sys/ScopedMpiInit.cc b/src/corecel/sys/ScopedMpiInit.cc index 1f139d95ea..29b75d2e74 100644 --- a/src/corecel/sys/ScopedMpiInit.cc +++ b/src/corecel/sys/ScopedMpiInit.cc @@ -26,7 +26,7 @@ namespace celeritas { //---------------------------------------------------------------------------// -// False positive with clang-tidy-15, need cleanup +// TODO Remove in clang-tidy-18 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) ScopedMpiInit::Status ScopedMpiInit::status_ = ScopedMpiInit::Status::uninitialized; diff --git a/src/corecel/sys/ScopedMpiInit.hh b/src/corecel/sys/ScopedMpiInit.hh index c9d495d769..e561271739 100644 --- a/src/corecel/sys/ScopedMpiInit.hh +++ b/src/corecel/sys/ScopedMpiInit.hh @@ -52,6 +52,8 @@ class ScopedMpiInit private: bool do_finalize_{false}; + // TODO Remove in clang-tidy-18 + // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) static Status status_; }; diff --git a/src/corecel/sys/Stream.hh b/src/corecel/sys/Stream.hh index b9f30047e2..4334eea533 100644 --- a/src/corecel/sys/Stream.hh +++ b/src/corecel/sys/Stream.hh @@ -24,6 +24,9 @@ struct MockMemoryResource virtual Pointer do_allocate(std::size_t, std::size_t) = 0; virtual void do_deallocate(Pointer, std::size_t, std::size_t) = 0; + + protected: + ~MockMemoryResource() = default; }; #endif diff --git a/src/geocel/g4/GeantGeoParams.hh b/src/geocel/g4/GeantGeoParams.hh index a551d2cd11..dbd7308662 100644 --- a/src/geocel/g4/GeantGeoParams.hh +++ b/src/geocel/g4/GeantGeoParams.hh @@ -9,6 +9,7 @@ #include +#include "corecel/Macros.hh" #include "corecel/Types.hh" #include "corecel/cont/LabelIdMultiMap.hh" #include "corecel/data/ParamsDataInterface.hh" @@ -45,8 +46,10 @@ class GeantGeoParams final : public GeoParamsInterface, // Create a VecGeom model from a pre-existing Geant4 geometry explicit GeantGeoParams(G4VPhysicalVolume const* world); + CELER_DEFAULT_MOVE_DELETE_COPY(GeantGeoParams); + // Clean up on destruction - ~GeantGeoParams(); + ~GeantGeoParams() final; //! Access the world volume G4VPhysicalVolume const* world() const { return host_ref_.world; } diff --git a/src/geocel/g4/GeantGeoTrackView.hh b/src/geocel/g4/GeantGeoTrackView.hh index 1ce5dc23d3..64cee1abb9 100644 --- a/src/geocel/g4/GeantGeoTrackView.hh +++ b/src/geocel/g4/GeantGeoTrackView.hh @@ -178,10 +178,10 @@ GeantGeoTrackView::GeantGeoTrackView(ParamsRef const&, , safety_radius_(states.safety_radius[tid]) , touch_handle_(states.nav_state.touch_handle(tid)) , navi_(states.nav_state.navigator(tid)) + , g4pos_(convert_to_geant(pos_, clhep_length)) + , g4dir_(convert_to_geant(dir_, 1)) + , g4safety_(convert_to_geant(safety_radius_, clhep_length)) { - g4pos_ = convert_to_geant(pos_, clhep_length); - g4dir_ = convert_to_geant(dir_, 1); - g4safety_ = convert_to_geant(safety_radius_, clhep_length); } //---------------------------------------------------------------------------// diff --git a/src/geocel/rasterize/RaytraceImager.hh b/src/geocel/rasterize/RaytraceImager.hh index 61df6700e0..2961d057c8 100644 --- a/src/geocel/rasterize/RaytraceImager.hh +++ b/src/geocel/rasterize/RaytraceImager.hh @@ -37,6 +37,7 @@ class RaytraceImager final : public ImagerInterface public: // Construct with geometry explicit RaytraceImager(SPGeometry geo); + ~RaytraceImager() final = default; // Raytrace an image on host or device void operator()(Image* image) final; @@ -70,6 +71,8 @@ class RaytraceImager final : public ImagerInterface //// MEMBER FUNCTIONS //// + // TODO Remove in clang-tidy-18 + // NOLINTNEXTLINE(performance-noexcept-move-constructor) CELER_DEFAULT_MOVE_DELETE_COPY(RaytraceImager); template diff --git a/src/orange/OrangeParams.cc b/src/orange/OrangeParams.cc index 550a1e60b9..5353b18115 100644 --- a/src/orange/OrangeParams.cc +++ b/src/orange/OrangeParams.cc @@ -219,7 +219,6 @@ OrangeParams::OrangeParams(OrangeInput&& input) */ OrangeParams::~OrangeParams() = default; -// NOLINTNEXTLINE(cppcoreguidelines-virtual-class-destructor) template class CollectionMirror; template class ParamsDataInterface; diff --git a/src/orange/detail/LevelStateAccessor.hh b/src/orange/detail/LevelStateAccessor.hh index 15788efacd..0fa9ab25d2 100644 --- a/src/orange/detail/LevelStateAccessor.hh +++ b/src/orange/detail/LevelStateAccessor.hh @@ -34,9 +34,12 @@ class LevelStateAccessor TrackSlotId tid, LevelId level_id); + LevelStateAccessor(LevelStateAccessor const&) = default; + LevelStateAccessor(LevelStateAccessor&&) = default; // Copy data from another LSA inline CELER_FUNCTION LevelStateAccessor& operator=(LevelStateAccessor const& other); + ~LevelStateAccessor() = default; //// ACCESSORS //// @@ -109,6 +112,10 @@ LevelStateAccessor::LevelStateAccessor(StateRef const* states, CELER_FUNCTION LevelStateAccessor& LevelStateAccessor::operator=(LevelStateAccessor const& other) { + if (this == &other) + { + return *this; + } this->vol() = other.vol(); this->pos() = other.pos(); this->dir() = other.dir(); diff --git a/src/orange/orangeinp/CsgTypes.hh b/src/orange/orangeinp/CsgTypes.hh index 98a1497d29..e42785af19 100644 --- a/src/orange/orangeinp/CsgTypes.hh +++ b/src/orange/orangeinp/CsgTypes.hh @@ -13,6 +13,8 @@ #include #include +#include "corecel/Config.hh" + #include "corecel/OpaqueId.hh" #include "corecel/math/HashUtils.hh" #include "orange/OrangeTypes.hh" @@ -205,7 +207,8 @@ struct hash { using argument_type = celeritas::orangeinp::Joined; using result_type = std::size_t; - result_type operator()(argument_type const& val) const noexcept + result_type operator()(argument_type const& val) const + noexcept(!CELERITAS_DEBUG) { result_type result; celeritas::Hasher hash{&result}; diff --git a/src/orange/orangeinp/IntersectRegion.hh b/src/orange/orangeinp/IntersectRegion.hh index a72106d914..7f8bed3bb8 100644 --- a/src/orange/orangeinp/IntersectRegion.hh +++ b/src/orange/orangeinp/IntersectRegion.hh @@ -50,11 +50,12 @@ class IntersectRegionInterface //! Write the region to a JSON object virtual void output(JsonPimpl*) const = 0; + virtual ~IntersectRegionInterface() = default; + protected: //!@{ //! Allow construction and assignment only through daughter classes IntersectRegionInterface() = default; - virtual ~IntersectRegionInterface() = default; CELER_DEFAULT_COPY_MOVE(IntersectRegionInterface); //!@} }; diff --git a/src/orange/orangeinp/PolySolid.cc b/src/orange/orangeinp/PolySolid.cc index 3427e75908..bd7a9a8eac 100644 --- a/src/orange/orangeinp/PolySolid.cc +++ b/src/orange/orangeinp/PolySolid.cc @@ -28,10 +28,9 @@ namespace //---------------------------------------------------------------------------// //! Construct the unioned "interior" of a polysolid template -[[nodiscard]] NodeId construct_segments( - PolySolidBase const& base, - T&& build_region, // NOLINT(cppcoreguidelines-missing-std-forward) - detail::VolumeBuilder& vb) +[[nodiscard]] NodeId construct_segments(PolySolidBase const& base, + T&& build_region, + detail::VolumeBuilder& vb) { std::string const label{base.label()}; auto const& segments = base.segments(); diff --git a/src/orange/orangeinp/PolySolid.hh b/src/orange/orangeinp/PolySolid.hh index 781d0c0305..55434cfa1a 100644 --- a/src/orange/orangeinp/PolySolid.hh +++ b/src/orange/orangeinp/PolySolid.hh @@ -120,7 +120,7 @@ class PolySolidBase : public ObjectInterface { public: // Anchored default virtual destructor - virtual ~PolySolidBase(); + ~PolySolidBase() override; //! Get the user-provided label std::string_view label() const final { return label_; } diff --git a/src/orange/orangeinp/ProtoInterface.hh b/src/orange/orangeinp/ProtoInterface.hh index e4fba6f97a..506926f593 100644 --- a/src/orange/orangeinp/ProtoInterface.hh +++ b/src/orange/orangeinp/ProtoInterface.hh @@ -71,7 +71,7 @@ class ProtoInterface //!@{ //! Allow construction and assignment only through subclasses ProtoInterface() = default; - virtual ~ProtoInterface() = default; + ~ProtoInterface() = default; CELER_DEFAULT_COPY_MOVE(ProtoInterface); //!@} }; diff --git a/src/orange/orangeinp/Shape.hh b/src/orange/orangeinp/Shape.hh index 0661104942..aa024008d4 100644 --- a/src/orange/orangeinp/Shape.hh +++ b/src/orange/orangeinp/Shape.hh @@ -47,11 +47,12 @@ class ShapeBase : public ObjectInterface //! Interior intersect region interface for construction and access virtual IntersectRegionInterface const& interior() const = 0; + ~ShapeBase() override = default; + protected: //!@{ //! Allow construction and assignment only through daughter classes ShapeBase() = default; - virtual ~ShapeBase() = default; CELER_DEFAULT_COPY_MOVE(ShapeBase); //!@} }; diff --git a/src/orange/orangeinp/Solid.hh b/src/orange/orangeinp/Solid.hh index 9038d243b1..9bf60dda45 100644 --- a/src/orange/orangeinp/Solid.hh +++ b/src/orange/orangeinp/Solid.hh @@ -96,11 +96,12 @@ class SolidBase : public ObjectInterface //! Optional azimuthal angular restriction virtual SolidEnclosedAngle enclosed_angle() const = 0; + ~SolidBase() override = default; + protected: //!@{ //! Allow construction and assignment only through daughter classes SolidBase() = default; - virtual ~SolidBase() = default; CELER_DEFAULT_COPY_MOVE(SolidBase); //!@} }; diff --git a/src/orange/orangeinp/UnitProto.hh b/src/orange/orangeinp/UnitProto.hh index 9e80ef870b..b7a09dd453 100644 --- a/src/orange/orangeinp/UnitProto.hh +++ b/src/orange/orangeinp/UnitProto.hh @@ -140,6 +140,8 @@ class UnitProto : public ProtoInterface // Construct with required input data explicit UnitProto(Input&& inp); + virtual ~UnitProto() = default; + // Short unique name of this object std::string_view label() const final; diff --git a/src/orange/orangeinp/detail/VolumeBuilder.hh b/src/orange/orangeinp/detail/VolumeBuilder.hh index 6a5696f186..74f1556631 100644 --- a/src/orange/orangeinp/detail/VolumeBuilder.hh +++ b/src/orange/orangeinp/detail/VolumeBuilder.hh @@ -7,6 +7,8 @@ //---------------------------------------------------------------------------// #pragma once +#include "corecel/Config.hh" + #include "corecel/io/Label.hh" #include "orange/transform/VariantTransform.hh" @@ -109,20 +111,25 @@ class PopVBTransformOnDestruct public: //! Capture the pointer when move constructed - PopVBTransformOnDestruct(PopVBTransformOnDestruct&& other) + PopVBTransformOnDestruct(PopVBTransformOnDestruct&& other) noexcept : vb_(std::exchange(other.vb_, nullptr)) { } //! Capture the pointer when move assigned - PopVBTransformOnDestruct& operator=(PopVBTransformOnDestruct&& other) + PopVBTransformOnDestruct& + operator=(PopVBTransformOnDestruct&& other) noexcept { vb_ = std::exchange(other.vb_, nullptr); return *this; } + PopVBTransformOnDestruct(PopVBTransformOnDestruct const&) = default; + PopVBTransformOnDestruct& operator=(PopVBTransformOnDestruct const&) + = default; + //! Call pop when we own the pointer and go out of scope - ~PopVBTransformOnDestruct() + ~PopVBTransformOnDestruct() noexcept(!CELERITAS_DEBUG) { if (vb_) { From cbf14de93c6ca397fa16de7d916f717dccd1d1ca Mon Sep 17 00:00:00 2001 From: "Seth R. Johnson" Date: Thu, 5 Dec 2024 09:43:11 -0500 Subject: [PATCH 10/15] Fix hepmc3/macro initialization in celer-g4 (#1535) * Note deprecation of .mac for celer-g4 * Add exception handling to primary creation * Default secondary stack factor to two * Move hepmc3 generator construction to action init and add exception conversion * Eliminate num_events and wrap HepMC3 initialization --- app/celer-g4/ActionInitialization.cc | 31 +++++++++++++++++++++------- app/celer-g4/ActionInitialization.hh | 9 ++++---- app/celer-g4/GlobalSetup.cc | 18 +++------------- app/celer-g4/GlobalSetup.hh | 11 ---------- app/celer-g4/RunInput.hh | 2 +- app/celer-g4/celer-g4.cc | 1 + doc/introduction/usage/app.rst | 5 +++-- 7 files changed, 36 insertions(+), 41 deletions(-) diff --git a/app/celer-g4/ActionInitialization.cc b/app/celer-g4/ActionInitialization.cc index 31b3f52761..76998c8bc0 100644 --- a/app/celer-g4/ActionInitialization.cc +++ b/app/celer-g4/ActionInitialization.cc @@ -8,10 +8,12 @@ #include "ActionInitialization.hh" #include "corecel/io/Logger.hh" +#include "accel/ExceptionConverter.hh" #include "accel/HepMC3PrimaryGenerator.hh" #include "accel/LocalTransporter.hh" #include "EventAction.hh" +#include "GeantDiagnostics.hh" #include "GlobalSetup.hh" #include "HepMC3PrimaryGeneratorAction.hh" #include "PGPrimaryGeneratorAction.hh" @@ -36,14 +38,18 @@ ActionInitialization::ActionInitialization(SPParams params) // Create Geant4 diagnostics to be shared across worker threads diagnostics_ = std::make_shared(); - if (auto const& hepmc_gen = GlobalSetup::Instance()->hepmc_gen()) + auto const& input = GlobalSetup::Instance()->input(); + if (!input.event_file.empty()) { - num_events_ = hepmc_gen->NumEvents(); + ExceptionConverter call_g4exception{"celer0007"}; + CELER_TRY_HANDLE(hepmc_gen_ = std::make_shared( + input.event_file), + call_g4exception); + num_events_ = hepmc_gen_->NumEvents(); } else { - num_events_ - = GlobalSetup::Instance()->input().primary_options.num_events; + num_events_ = input.primary_options.num_events; } CELER_ENSURE(num_events_ > 0); @@ -82,15 +88,24 @@ void ActionInitialization::Build() const CELER_LOG_LOCAL(status) << "Constructing user action"; // Primary generator emits source particles - if (auto const& hepmc_gen = GlobalSetup::Instance()->hepmc_gen()) + std::unique_ptr generator_action; + if (hepmc_gen_) { - this->SetUserAction(new HepMC3PrimaryGeneratorAction(hepmc_gen)); + ExceptionConverter call_g4exception{"celer0007"}; + CELER_TRY_HANDLE( + generator_action + = std::make_unique(hepmc_gen_), + call_g4exception); } else { - this->SetUserAction(new PGPrimaryGeneratorAction( - GlobalSetup::Instance()->input().primary_options)); + ExceptionConverter call_g4exception{"celer0006"}; + CELER_TRY_HANDLE( + generator_action = std::make_unique( + GlobalSetup::Instance()->input().primary_options), + call_g4exception); } + this->SetUserAction(generator_action.release()); // Create thread-local transporter to share between actions auto transport = std::make_shared(); diff --git a/app/celer-g4/ActionInitialization.hh b/app/celer-g4/ActionInitialization.hh index 4d83dcb4e8..b1b9facf0e 100644 --- a/app/celer-g4/ActionInitialization.hh +++ b/app/celer-g4/ActionInitialization.hh @@ -12,12 +12,13 @@ #include "accel/SharedParams.hh" -#include "GeantDiagnostics.hh" - namespace celeritas { +class HepMC3PrimaryGenerator; + namespace app { +class GeantDiagnostics; //---------------------------------------------------------------------------// /*! * Set up demo-specific action initializations. @@ -28,7 +29,6 @@ class ActionInitialization final : public G4VUserActionInitialization //!@{ //! \name Type aliases using SPParams = std::shared_ptr; - using SPDiagnostics = std::shared_ptr; //!@} public: @@ -41,7 +41,8 @@ class ActionInitialization final : public G4VUserActionInitialization private: SPParams params_; - SPDiagnostics diagnostics_; + std::shared_ptr diagnostics_; + std::shared_ptr hepmc_gen_; int num_events_{0}; mutable bool init_shared_{true}; }; diff --git a/app/celer-g4/GlobalSetup.cc b/app/celer-g4/GlobalSetup.cc index ceb65b87c7..07a4c583fd 100644 --- a/app/celer-g4/GlobalSetup.cc +++ b/app/celer-g4/GlobalSetup.cc @@ -164,21 +164,9 @@ void GlobalSetup::ReadInput(std::string const& filename) // Apply Celeritas \c SetupOptions commands options_->max_num_tracks = input_.num_track_slots; - options_->max_num_events = [this] { - CELER_VALIDATE(input_.primary_options || !input_.event_file.empty(), - << "no event input file nor primary options were " - "specified"); - if (!input_.event_file.empty()) - { - hepmc_gen_ = std::make_shared( - input_.event_file); - return static_cast(hepmc_gen_->NumEvents()); - } - else - { - return input_.primary_options.num_events; - } - }(); + CELER_VALIDATE(input_.primary_options || !input_.event_file.empty(), + << "no event input file nor primary options were " + "specified"); options_->max_steps = input_.max_steps; options_->initializer_capacity = input_.initializer_capacity; options_->secondary_stack_factor = input_.secondary_stack_factor; diff --git a/app/celer-g4/GlobalSetup.hh b/app/celer-g4/GlobalSetup.hh index 96c1e219f9..f556c670eb 100644 --- a/app/celer-g4/GlobalSetup.hh +++ b/app/celer-g4/GlobalSetup.hh @@ -23,7 +23,6 @@ class G4GenericMessenger; namespace celeritas { -class HepMC3PrimaryGenerator; namespace app { //---------------------------------------------------------------------------// @@ -32,12 +31,6 @@ namespace app */ class GlobalSetup { - public: - //!@{ - //! \name Type aliases - using SPPrimaryGenerator = std::shared_ptr; - //!@} - public: // Return non-owning pointer to a singleton static GlobalSetup* Instance(); @@ -98,9 +91,6 @@ class GlobalSetup //! Whether ROOT I/O for SDs is enabled bool root_sd_io() const { return root_sd_io_; } - //! Get HepMC3 primary generator - SPPrimaryGenerator const& hepmc_gen() const { return hepmc_gen_; } - private: // Private constructor since we're a singleton GlobalSetup(); @@ -108,7 +98,6 @@ class GlobalSetup // Data std::shared_ptr options_; - SPPrimaryGenerator hepmc_gen_; RunInput input_; Stopwatch get_setup_time_; bool root_sd_io_{false}; diff --git a/app/celer-g4/RunInput.hh b/app/celer-g4/RunInput.hh index 11189ff8cf..9a5f09a563 100644 --- a/app/celer-g4/RunInput.hh +++ b/app/celer-g4/RunInput.hh @@ -77,7 +77,7 @@ struct RunInput size_type num_track_slots{}; size_type max_steps{unspecified}; size_type initializer_capacity{}; - real_type secondary_stack_factor{}; + real_type secondary_stack_factor{2}; size_type auto_flush{}; //!< Defaults to num_track_slots bool action_times{false}; diff --git a/app/celer-g4/celer-g4.cc b/app/celer-g4/celer-g4.cc index caca6d8252..656aada2aa 100644 --- a/app/celer-g4/celer-g4.cc +++ b/app/celer-g4/celer-g4.cc @@ -38,6 +38,7 @@ #include "corecel/Macros.hh" #include "corecel/io/ExceptionOutput.hh" #include "corecel/io/Logger.hh" +#include "corecel/io/OutputRegistry.hh" #include "corecel/io/ScopedTimeAndRedirect.hh" #include "corecel/io/ScopedTimeLog.hh" #include "corecel/io/StringUtils.hh" diff --git a/doc/introduction/usage/app.rst b/doc/introduction/usage/app.rst index 646096a14c..f451359f23 100644 --- a/doc/introduction/usage/app.rst +++ b/doc/introduction/usage/app.rst @@ -80,8 +80,9 @@ specified with a combination of the ``field_type``, ``field``, and ``field_file`` keys, and detailed field driver configuration options are set with ``field_options`` corresponding to the ``FieldOptions`` class in :ref:`api_field_data`. -.. note:: The macro file usage is in the process of being replaced by JSON - input for improved automation. +.. deprecated:: The macro file usage is in the process of being replaced by JSON + input for improved automation. Until then, refer + to the source code at :file:`app/celer-g4/RunInput.hh` . The input is a Geant4 macro file for executing the program. Celeritas defines several macros in the ``/celer`` and (if CUDA is available) ``/celer/cuda/`` From e88f25a86acc7b6363f3345eb1525eb9c671bbb8 Mon Sep 17 00:00:00 2001 From: Hayden Hollenbeck <127582780+hhollenb@users.noreply.github.com> Date: Thu, 5 Dec 2024 11:16:43 -0600 Subject: [PATCH 11/15] Clean up optical mock test data (#1519) * Refactored optical mock tests to use GlobalTestBase, and to split validation checks into separate file * Added missing includes to ValidationUtils.hh * Changed templated unit constructors in optical tests to not explicitly require real_types * Added some of Seth's suggested changes * Test of using googletest formatters for grids and tables * Cleaned up changes to GridAccessor and formatting * Removed dangling override of IsVecEq in ValidationUtils.cc * Fixed size_t vs size_type in native_array_from * Changed native_array_from signature to avoid collisions * Changed native_array_from_indexer to use integer sequence to support size_type instead of size_t * Removed native_array_from for fixed size array argument * Sort optical tests in cmakelists --- src/celeritas/UnitTypes.hh | 14 + test/celeritas/CMakeLists.txt | 15 +- test/celeritas/optical/Absorption.test.cc | 26 +- .../optical/ImportedModelAdapter.test.cc | 36 +- test/celeritas/optical/MfpBuilder.test.cc | 17 +- test/celeritas/optical/MockImportedData.cc | 319 ------------------ test/celeritas/optical/MockImportedData.hh | 143 -------- test/celeritas/optical/MockValidation.test.cc | 66 ---- test/celeritas/optical/OpticalMockTestBase.cc | 273 +++++++++++++++ test/celeritas/optical/OpticalMockTestBase.hh | 77 +++++ test/celeritas/optical/Rayleigh.test.cc | 27 +- .../optical/RayleighMfpCalculator.test.cc | 86 ++--- test/celeritas/optical/ValidationUtils.cc | 82 +++++ test/celeritas/optical/ValidationUtils.hh | 122 +++++++ .../optical/detail/ValidationUtilsImpl.hh | 165 +++++++++ 15 files changed, 825 insertions(+), 643 deletions(-) delete mode 100644 test/celeritas/optical/MockImportedData.cc delete mode 100644 test/celeritas/optical/MockImportedData.hh delete mode 100644 test/celeritas/optical/MockValidation.test.cc create mode 100644 test/celeritas/optical/OpticalMockTestBase.cc create mode 100644 test/celeritas/optical/OpticalMockTestBase.hh create mode 100644 test/celeritas/optical/ValidationUtils.cc create mode 100644 test/celeritas/optical/ValidationUtils.hh create mode 100644 test/celeritas/optical/detail/ValidationUtilsImpl.hh diff --git a/src/celeritas/UnitTypes.hh b/src/celeritas/UnitTypes.hh index 240ec94ffd..2732083a6f 100644 --- a/src/celeritas/UnitTypes.hh +++ b/src/celeritas/UnitTypes.hh @@ -53,6 +53,20 @@ struct EElectron //!@{ //! \name Atomic units +//! Atom-scale energy +struct ElectronVolt +{ + static CELER_CONSTEXPR_FUNCTION real_type value() + { +#if CELERITAS_UNITS == CELERITAS_UNITS_CLHEP + return units::megaelectronvolt / real_type(1e6); +#else + return constants::e_electron * units::volt; +#endif + } + static char const* label() { return "eV"; } +}; + //! Nucleus-scale energy struct Mev { diff --git a/test/celeritas/CMakeLists.txt b/test/celeritas/CMakeLists.txt index 2d25a7e7e3..4617e49ead 100644 --- a/test/celeritas/CMakeLists.txt +++ b/test/celeritas/CMakeLists.txt @@ -79,9 +79,10 @@ celeritas_add_test_library(testcel_celeritas grid/CalculatorTestBase.cc io/EventIOTestBase.cc neutron/NeutronTestBase.cc - optical/MockImportedData.cc + optical/OpticalMockTestBase.cc optical/OpticalTestBase.cc optical/InteractorHostTestBase.cc + optical/ValidationUtils.cc phys/InteractionIO.cc phys/InteractorHostTestBase.cc phys/MockModel.cc @@ -306,17 +307,17 @@ celeritas_add_test(io/SeltzerBergerReader.test.cc ${_needs_geant4}) #-----------------------------------------------------------------------------# # Optical + +celeritas_add_test(optical/Absorption.test.cc) celeritas_add_test(optical/Cherenkov.test.cc) +celeritas_add_test(optical/ImportedModelAdapter.test.cc) +celeritas_add_test(optical/MfpBuilder.test.cc) celeritas_add_test(optical/OpticalCollector.test.cc ${_needs_geant4}) celeritas_add_test(optical/OpticalUtils.test.cc) +celeritas_add_test(optical/Rayleigh.test.cc) +celeritas_add_test(optical/RayleighMfpCalculator.test.cc) celeritas_add_test(optical/Scintillation.test.cc) -celeritas_add_test(optical/Rayleigh.test.cc ${_needs_double}) -celeritas_add_test(optical/Absorption.test.cc ${_needs_double}) celeritas_add_test(optical/WavelengthShift.test.cc ${_needs_double}) -celeritas_add_test(optical/ImportedModelAdapter.test.cc) -celeritas_add_test(optical/MfpBuilder.test.cc) -celeritas_add_test(optical/RayleighMfpCalculator.test.cc) -celeritas_add_test(optical/MockValidation.test.cc) #-----------------------------------------------------------------------------# # Mat diff --git a/test/celeritas/optical/Absorption.test.cc b/test/celeritas/optical/Absorption.test.cc index 491935add3..f86459a46c 100644 --- a/test/celeritas/optical/Absorption.test.cc +++ b/test/celeritas/optical/Absorption.test.cc @@ -8,7 +8,8 @@ #include "celeritas/optical/interactor/AbsorptionInteractor.hh" #include "celeritas/optical/model/AbsorptionModel.hh" -#include "MockImportedData.hh" +#include "OpticalMockTestBase.hh" +#include "ValidationUtils.hh" #include "celeritas_test.hh" namespace celeritas @@ -28,7 +29,7 @@ class AbsorptionInteractorTest : public ::celeritas::test::Test void SetUp() override {} }; -class AbsorptionModelTest : public MockImportedData +class AbsorptionModelTest : public OpticalMockTestBase { protected: void SetUp() override {} @@ -36,15 +37,10 @@ class AbsorptionModelTest : public MockImportedData //! Construct absorption model from mock data std::shared_ptr create_model() { - auto models = MockImportedData::create_imported_models(); - - import_model_id_ - = models->builtin_model_id(ImportModelClass::absorption); - + auto models = std::make_shared( + this->imported_data().optical_models); return std::make_shared(ActionId{0}, models); } - - ImportedModels::ImportedModelId import_model_id_; }; //---------------------------------------------------------------------------// @@ -80,17 +76,19 @@ TEST_F(AbsorptionModelTest, description) // Check absorption model MFP tables match imported ones TEST_F(AbsorptionModelTest, interaction_mfp) { + OwningGridAccessor storage; + auto model = create_model(); - auto builder = this->create_mfp_builder(); + auto builder = storage.create_mfp_builder(); - for (auto mat : range(OpticalMaterialId(import_materials().size()))) + for (auto mat : range(OpticalMaterialId(this->num_optical_materials()))) { model->build_mfps(mat, builder); } - this->check_built_table_exact( - this->import_models()[import_model_id_.get()].mfp_table, - builder.grid_ids()); + EXPECT_TABLE_EQ( + this->import_model_by_class(ImportModelClass::absorption).mfp_table, + storage(builder.grid_ids())); } //---------------------------------------------------------------------------// diff --git a/test/celeritas/optical/ImportedModelAdapter.test.cc b/test/celeritas/optical/ImportedModelAdapter.test.cc index 11ed4e5b26..f2e3d5e905 100644 --- a/test/celeritas/optical/ImportedModelAdapter.test.cc +++ b/test/celeritas/optical/ImportedModelAdapter.test.cc @@ -13,7 +13,8 @@ #include "celeritas/ext/ScopedRootErrorHandler.hh" #include "celeritas/io/ImportData.hh" -#include "MockImportedData.hh" +#include "OpticalMockTestBase.hh" +#include "ValidationUtils.hh" #include "celeritas_test.hh" namespace celeritas @@ -27,10 +28,10 @@ using namespace ::celeritas::test; // TEST HARNESS //---------------------------------------------------------------------------// -class ImportedModelAdapterTest : public MockImportedData +class ImportedModelAdapterTest : public OpticalMockTestBase { protected: - void SetUp() override {} + using ImportedModelId = typename ImportedModels::ImportedModelId; void check_model(ImportOpticalModel const& expected_model, ImportOpticalModel const& imported_model) const @@ -40,10 +41,21 @@ class ImportedModelAdapterTest : public MockImportedData imported_model.mfp_table.size()); for (auto mat_id : range(imported_model.mfp_table.size())) { - this->check_mfp(expected_model.mfp_table[mat_id], - imported_model.mfp_table[mat_id]); + EXPECT_GRID_EQ(expected_model.mfp_table[mat_id], + imported_model.mfp_table[mat_id]); } } + + std::shared_ptr const& imported_models() const + { + static std::shared_ptr models; + if (!models) + { + models = std::make_shared( + this->imported_data().optical_models); + } + return models; + } }; //---------------------------------------------------------------------------// @@ -52,8 +64,8 @@ class ImportedModelAdapterTest : public MockImportedData // Create ImportedModels from mock data TEST_F(ImportedModelAdapterTest, build_mock) { - auto const& expected_models = this->import_models(); - auto imported_models = this->create_imported_models(); + auto const& expected_models = this->imported_data().optical_models; + auto imported_models = this->imported_models(); ASSERT_EQ(expected_models.size(), imported_models->num_models()); for (auto model_id : range(ImportedModelId{imported_models->num_models()})) @@ -71,7 +83,7 @@ TEST_F(ImportedModelAdapterTest, builtin_map) std::array expected_builtin_imcs{ IMC::absorption, IMC::rayleigh, IMC::wls}; - auto imported_models = this->create_imported_models(); + auto imported_models = this->imported_models(); // Check built-in models match expected ones EXPECT_EQ(expected_builtin_imcs.size(), static_cast(IMC::size_)); @@ -89,8 +101,8 @@ TEST_F(ImportedModelAdapterTest, builtin_map) // Check adapters correctly match MFPs TEST_F(ImportedModelAdapterTest, adapter_mfps) { - auto const& expected_models = this->import_models(); - auto imported_models = this->create_imported_models(); + auto const& expected_models = this->imported_data().optical_models; + auto imported_models = this->imported_models(); ASSERT_EQ(expected_models.size(), imported_models->num_models()); for (auto model_id : range(ImportedModelId{imported_models->num_models()})) @@ -101,8 +113,8 @@ TEST_F(ImportedModelAdapterTest, adapter_mfps) ASSERT_EQ(expected_model.mfp_table.size(), adapter.num_materials()); for (auto mat_id : range(OpticalMaterialId{adapter.num_materials()})) { - this->check_mfp(expected_model.mfp_table[mat_id.get()], - adapter.mfp(mat_id)); + EXPECT_GRID_EQ(expected_model.mfp_table[mat_id.get()], + adapter.mfp(mat_id)); } } } diff --git a/test/celeritas/optical/MfpBuilder.test.cc b/test/celeritas/optical/MfpBuilder.test.cc index 3efe8a4878..87918924a0 100644 --- a/test/celeritas/optical/MfpBuilder.test.cc +++ b/test/celeritas/optical/MfpBuilder.test.cc @@ -7,7 +7,8 @@ //---------------------------------------------------------------------------// #include "celeritas/optical/MfpBuilder.hh" -#include "MockImportedData.hh" +#include "OpticalMockTestBase.hh" +#include "ValidationUtils.hh" #include "celeritas_test.hh" namespace celeritas @@ -21,10 +22,8 @@ using namespace ::celeritas::test; // TEST HARNESS //---------------------------------------------------------------------------// -class MfpBuilderTest : public MockImportedData +class MfpBuilderTest : public OpticalMockTestBase { - protected: - void SetUp() override {} }; //---------------------------------------------------------------------------// @@ -33,13 +32,15 @@ class MfpBuilderTest : public MockImportedData // Check MFP tables are built with correct structure from imported data TEST_F(MfpBuilderTest, construct_tables) { - std::vector> tables; - auto const& models = this->import_models(); + OwningGridAccessor storage; + + std::vector> tables; + auto const& models = this->imported_data().optical_models; // Build MFP tables from imported data for (auto const& model : models) { - auto build = this->create_mfp_builder(); + auto build = storage.create_mfp_builder(); for (auto const& mfp : model.mfp_table) { @@ -54,7 +55,7 @@ TEST_F(MfpBuilderTest, construct_tables) // Check each MFP table has been built correctly for (auto table_id : range(tables.size())) { - this->check_built_table_exact(models[table_id].mfp_table, tables[table_id]); + EXPECT_TABLE_EQ(models[table_id].mfp_table, storage(tables[table_id])); } } diff --git a/test/celeritas/optical/MockImportedData.cc b/test/celeritas/optical/MockImportedData.cc deleted file mode 100644 index 3d8d78a519..0000000000 --- a/test/celeritas/optical/MockImportedData.cc +++ /dev/null @@ -1,319 +0,0 @@ -//----------------------------------*-C++-*----------------------------------// -// Copyright 2024 UT-Battelle, LLC, and other Celeritas developers. -// See the top-level COPYRIGHT file for details. -// SPDX-License-Identifier: (Apache-2.0 OR MIT) -//---------------------------------------------------------------------------// -//! \file celeritas/optical/MockImportedData.cc -//---------------------------------------------------------------------------// -#include "MockImportedData.hh" - -#include "TestMacros.hh" - -namespace celeritas -{ -namespace optical -{ -namespace test -{ -//---------------------------------------------------------------------------// -struct MeterCubedPerMeV -{ - static CELER_CONSTEXPR_FUNCTION real_type value() - { - return ipow<3>(units::meter) / units::Mev::value(); - } - - static char const* label() { return "m^3/MeV"; } -}; - -using IsothermalCompressibility = Quantity; - -//---------------------------------------------------------------------------// -/*! - * Convert vector of some floating type to a vector of reals. - */ -template -std::vector convert_to_reals(std::vector const& xs) -{ - std::vector reals; - reals.reserve(xs.size()); - for (T x : xs) - { - reals.push_back(static_cast(x)); - } - return reals; -} - -//---------------------------------------------------------------------------// -/*! - * Create some mock physics vectors. - * - * For x grid: - * i = 0,1: size = 2 - * i = 2,3: size = 3 - * - * For y values: - * j = 0,1,2: size = 2 - * j = 3,4: size = 3 - */ -std::vector -mock_vec(std::vector const& grid_indices, - std::vector const& value_indices) -{ - static std::vector> grids{ - {1e-3, 1e2}, {1e-2, 3e2}, {2e-3, 5e-1, 1e2}, {1e-3, 2e-3, 5e-1}}; - - static std::vector> values{ - {5.7, 9.3}, - {1.2, 10.7}, - {3.1, 5.4}, - {0.1, 7.6, 12.5}, - {1.3, 4.9, 9.4}, - }; - - std::vector> mock_grids; - for (auto i : grid_indices) - { - CELER_ASSERT(i < grids.size()); - mock_grids.push_back(grids[i]); - } - - std::vector> mock_values; - for (auto j : value_indices) - { - CELER_ASSERT(j < values.size()); - mock_values.push_back(values[j]); - } - - return detail::convert_vector_units( - mock_grids, mock_values); -} - -//---------------------------------------------------------------------------// -/*! - * Construct vector of ImportOpticalModel from mock data. - * - * There are 4 imported models, one for each optical model class. All models - * have MFP grids for 5 materials. - */ -std::vector const& MockImportedData::import_models() -{ - using IMC = ImportModelClass; - - static std::vector models{ - {IMC::absorption, mock_vec({0, 1, 1, 2, 3}, {0, 1, 2, 3, 4})}, - {IMC::rayleigh, mock_vec({1, 0, 3, 2, 0}, {0, 1, 3, 3, 2})}, - {IMC::wls, mock_vec({3, 1, 1, 2, 3}, {4, 0, 1, 4, 4})}}; - - return models; -} - -//---------------------------------------------------------------------------// -/*! - * Construct vector of ImportOpticalMaterial from mock data. - */ -std::vector const& MockImportedData::import_materials() -{ - using namespace celeritas::units; - - static std::vector> mock_energies - = {{1.098177, 1.256172, 1.484130}, {1.098177, 6.812319}, {1, 2, 5}}; - - static std::vector> mock_rindex - = {{1.3235601610672, 1.3256740639273, 1.3280120256415}, - {1.3235601610672, 1.4679465862259}, - {1.3, 1.4, 1.5}}; - - auto properties - = detail::convert_vector_units( - mock_energies, mock_rindex); - - static ImportOpticalRayleigh mock_rayleigh[] - = {{1, 7.658e-23 * MeterCubedPerMeV::value()}, - {1.7, 4.213e-24 * MeterCubedPerMeV::value()}, - {2, 1e-20 * MeterCubedPerMeV::value()}}; - - static std::vector materials{ - ImportOpticalMaterial{ImportOpticalProperty{properties[0]}, - ImportScintData{}, - ImportOpticalRayleigh{mock_rayleigh[0]}, - ImportWavelengthShift{}}, - ImportOpticalMaterial{ImportOpticalProperty{properties[0]}, - ImportScintData{}, - ImportOpticalRayleigh{mock_rayleigh[1]}, - ImportWavelengthShift{}}, - ImportOpticalMaterial{ImportOpticalProperty{properties[1]}, - ImportScintData{}, - ImportOpticalRayleigh{mock_rayleigh[0]}, - ImportWavelengthShift{}}, - ImportOpticalMaterial{ImportOpticalProperty{properties[2]}, - ImportScintData{}, - ImportOpticalRayleigh{mock_rayleigh[2]}, - ImportWavelengthShift{}}, - ImportOpticalMaterial{ImportOpticalProperty{properties[1]}, - ImportScintData{}, - ImportOpticalRayleigh{mock_rayleigh[1]}, - ImportWavelengthShift{}}}; - - return materials; -} - -//---------------------------------------------------------------------------// -/*! - * Retrieve optical materials constructed from mock imported data. - * - * Will only construct the materials when called, and only once. - */ -auto MockImportedData::optical_materials() const -> SPConstMaterials const& -{ - static SPConstMaterials materials = nullptr; - if (!materials) - { - materials = this->build_optical_materials(); - } - return materials; -} - -//---------------------------------------------------------------------------// -/*! - * Build optical material parameters from imported data. - */ -auto MockImportedData::build_optical_materials() const -> SPConstMaterials -{ - MaterialParams::Input input; - for (auto mat : MockImportedData::import_materials()) - { - input.properties.push_back(mat.properties); - } - - // Volume -> optical material mapping with some redundancies - for (auto opt_mat : range(8)) - { - input.volume_to_mat.push_back( - OpticalMaterialId(opt_mat % input.properties.size())); - } - - return std::make_shared(std::move(input)); -} - -//---------------------------------------------------------------------------// -/*! - * Create ImportedModels all with empty MFP grids. - * - * Useful for testing optical models which build their MFPs from material data. - */ -auto MockImportedData::create_empty_imported_models() const -> SPConstImported -{ - std::vector empty_models; - empty_models.reserve(this->import_models().size()); - ImportPhysicsVector const empty_vec{ImportPhysicsVectorType::free, {}, {}}; - for (auto const& model : this->import_models()) - { - empty_models.push_back( - {model.model_class, - ImportedMfpTable(model.mfp_table.size(), empty_vec)}); - } - - return std::make_shared(std::move(empty_models)); -} - -//---------------------------------------------------------------------------// -/*! - * Create ImportedModels from mock data. - */ -auto MockImportedData::create_imported_models() const -> SPConstImported -{ - return std::make_shared(this->import_models()); -} - -//---------------------------------------------------------------------------// -/*! - * Create an MFP builder that uses this object's collections. - */ -auto MockImportedData::create_mfp_builder() -> MfpBuilder -{ - return MfpBuilder(&reals, &grids); -} - -//---------------------------------------------------------------------------// -/*! - * Check that two MFP physics vectors are equal. - */ -void MockImportedData::check_mfp(ImportPhysicsVector const& expected, - ImportPhysicsVector const& imported) const -{ - EXPECT_EQ(expected.vector_type, imported.vector_type); - EXPECT_VEC_EQ(expected.x, imported.x); - EXPECT_VEC_EQ(expected.y, imported.y); -} - -//---------------------------------------------------------------------------// -/*! - * Check that the physics table built in the collections matches the - * imported MFP table it was built from. - */ -void MockImportedData::check_built_table(ImportedMfpTable const& expected_mfps, - ItemRange const& table, - bool soft) const -{ - // Each MFP has a built grid - ASSERT_EQ(expected_mfps.size(), table.size()); - - for (auto mfp_id : range(expected_mfps.size())) - { - // Grid IDs should be valid - auto grid_id = table[mfp_id]; - ASSERT_LT(grid_id, grids.size()); - - // Grid should be valid - Grid const& grid = grids[grid_id]; - ASSERT_TRUE(grid); - - // Grid ranges should be valid - ASSERT_LT(grid.grid.back(), reals.size()); - ASSERT_LT(grid.value.back(), reals.size()); - - // Convert imported data to real_type for comparison - auto const& expected_mfp = expected_mfps[mfp_id]; - std::vector expected_grid = convert_to_reals(expected_mfp.x); - std::vector expected_value - = convert_to_reals(expected_mfp.y); - - // Built grid data should match expected grid data - if (soft) - { - EXPECT_VEC_SOFT_EQ(expected_grid, reals[grid.grid]); - EXPECT_VEC_SOFT_EQ(expected_value, reals[grid.value]); - } - else - { - EXPECT_VEC_EQ(expected_grid, reals[grid.grid]); - EXPECT_VEC_EQ(expected_value, reals[grid.value]); - } - } -} - -//---------------------------------------------------------------------------// -/*! - * Check the built physics table with soft equality. - */ -void MockImportedData::check_built_table_soft(ImportedMfpTable const& expected, - ItemRange const& table) const -{ - this->check_built_table(expected, table, true); -} - -//---------------------------------------------------------------------------// -/*! - * Check the built physics table with exact equality. - */ -void MockImportedData::check_built_table_exact( - ImportedMfpTable const& expected, ItemRange const& table) const -{ - this->check_built_table(expected, table, false); -} - -//---------------------------------------------------------------------------// -} // namespace test -} // namespace optical -} // namespace celeritas diff --git a/test/celeritas/optical/MockImportedData.hh b/test/celeritas/optical/MockImportedData.hh deleted file mode 100644 index afefe7fc2f..0000000000 --- a/test/celeritas/optical/MockImportedData.hh +++ /dev/null @@ -1,143 +0,0 @@ -//----------------------------------*-C++-*----------------------------------// -// Copyright 2024 UT-Battelle, LLC, and other Celeritas developers. -// See the top-level COPYRIGHT file for details. -// SPDX-License-Identifier: (Apache-2.0 OR MIT) -//---------------------------------------------------------------------------// -//! \file celeritas/optical/MockImportedData.hh -//---------------------------------------------------------------------------// -#pragma once - -#include "corecel/data/Collection.hh" -#include "celeritas/UnitTypes.hh" -#include "celeritas/grid/GenericGridData.hh" -#include "celeritas/io/ImportOpticalMaterial.hh" -#include "celeritas/io/ImportOpticalModel.hh" -#include "celeritas/io/ImportPhysicsVector.hh" -#include "celeritas/optical/ImportedModelAdapter.hh" -#include "celeritas/optical/MaterialParams.hh" -#include "celeritas/optical/MfpBuilder.hh" - -#include "Test.hh" - -namespace celeritas -{ -namespace optical -{ -namespace test -{ -using namespace ::celeritas::test; - -//---------------------------------------------------------------------------// -/*! - * Imported mock optical data. - * - * A base class that provides common mock data and functionality for testing - * optical physics. - */ -class MockImportedData : public ::celeritas::test::Test -{ - protected: - //!@{ - //! \name Type aliases - using Grid = GenericGridRecord; - using GridId = OpaqueId; - - using ImportedMfpTable = std::vector; - - using ImportedModelId = typename ImportedModels::ImportedModelId; - using SPConstImported = std::shared_ptr; - using SPConstMaterials = std::shared_ptr; - - template - using Items = Collection; - //!@} - - protected: - //!@{ - //! \name Access mock data - static std::vector const& import_models(); - static std::vector const& import_materials(); - std::shared_ptr const& optical_materials() const; - //!@} - - //!@{ - //! \name Construct commonly used objects - SPConstImported create_empty_imported_models() const; - SPConstImported create_imported_models() const; - MfpBuilder create_mfp_builder(); - SPConstMaterials build_optical_materials() const; - //!@} - - //!@{ - //! \name Check results - void check_mfp(ImportPhysicsVector const& expected, - ImportPhysicsVector const& imported) const; - void check_built_table_exact(ImportedMfpTable const& expected, - ItemRange const& table) const; - void check_built_table_soft(ImportedMfpTable const& expected, - ItemRange const& table) const; - void check_built_table(ImportedMfpTable const& expected, - ItemRange const& table, - bool soft) const; - //!@} - - //!@{ - //! \name Storage data - Items reals; - Items grids; - //!@} -}; - -namespace detail -{ -//---------------------------------------------------------------------------// -/*! - * Useful unit for working in optical physics scales. - */ -struct ElectronVolt -{ - static CELER_CONSTEXPR_FUNCTION real_type value() - { - return units::Mev::value() / 1e6; - } - static char const* label() { return "eV"; } -}; - -//---------------------------------------------------------------------------// -/*! - * Takes a list of grids and values in the specified template units, - * and converts to an ImportPhysicsVector of in Celeritas' units. - * - * The grid x units are returned in [MeV] rather than native, matching - * the usual unit for imported grids. - */ -template -std::vector -convert_vector_units(std::vector> const& grid, - std::vector> const& value) -{ - CELER_ASSERT(grid.size() == value.size()); - std::vector vecs; - for (auto i : range(grid.size())) - { - ImportPhysicsVector v; - v.vector_type = ImportPhysicsVectorType::free; - for (double x : grid[i]) - { - v.x.push_back(x * GridUnit::value() / units::Mev::value()); - } - for (double y : value[i]) - { - v.y.push_back(y * ValueUnit::value()); - } - CELER_ASSERT(v); - vecs.push_back(std::move(v)); - } - return vecs; -} - -//---------------------------------------------------------------------------// -} // namespace detail -} // namespace test -} // namespace optical -} // namespace celeritas diff --git a/test/celeritas/optical/MockValidation.test.cc b/test/celeritas/optical/MockValidation.test.cc deleted file mode 100644 index 7e0c83e4a0..0000000000 --- a/test/celeritas/optical/MockValidation.test.cc +++ /dev/null @@ -1,66 +0,0 @@ -//----------------------------------*-C++-*----------------------------------// -// Copyright 2024 UT-Battelle, LLC, and other Celeritas developers. -// See the top-level COPYRIGHT file for details. -// SPDX-License-Identifier: (Apache-2.0 OR MIT) -//---------------------------------------------------------------------------// -//! \file celeritas/optical/MockValidation.test.cc -//---------------------------------------------------------------------------// -#include "MockImportedData.hh" -#include "celeritas_test.hh" - -namespace celeritas -{ -namespace optical -{ -namespace test -{ -using namespace ::celeritas::test; -//---------------------------------------------------------------------------// -// TEST HARNESS -//---------------------------------------------------------------------------// - -class MockValidationTest : public MockImportedData -{ - protected: - void SetUp() override {} -}; - -//---------------------------------------------------------------------------// -// TESTS -//---------------------------------------------------------------------------// -// Validate that the mock optical data makes sense -TEST_F(MockValidationTest, validate) -{ - auto const& models = import_models(); - auto const& materials = import_materials(); - - EXPECT_EQ(3, models.size()); - EXPECT_EQ(5, materials.size()); - - // Check models - - for (auto const& model : models) - { - EXPECT_NE(ImportModelClass::size_, model.model_class); - EXPECT_EQ(materials.size(), model.mfp_table.size()); - - for (auto const& mfp : model.mfp_table) - { - EXPECT_EQ(ImportPhysicsVectorType::free, mfp.vector_type); - EXPECT_TRUE(mfp); - } - } - - // Check materials - - for (auto const& material : materials) - { - EXPECT_TRUE(material.properties); - EXPECT_TRUE(material.rayleigh); - } -} - -//---------------------------------------------------------------------------// -} // namespace test -} // namespace optical -} // namespace celeritas diff --git a/test/celeritas/optical/OpticalMockTestBase.cc b/test/celeritas/optical/OpticalMockTestBase.cc new file mode 100644 index 0000000000..ecbd9a5966 --- /dev/null +++ b/test/celeritas/optical/OpticalMockTestBase.cc @@ -0,0 +1,273 @@ +//----------------------------------*-C++-*----------------------------------// +// Copyright 2024 UT-Battelle, LLC, and other Celeritas developers. +// See the top-level COPYRIGHT file for details. +// SPDX-License-Identifier: (Apache-2.0 OR MIT) +//---------------------------------------------------------------------------// +//! \file celeritas/optical/OpticalMockTestBase.cc +//---------------------------------------------------------------------------// +#include "OpticalMockTestBase.hh" + +#include "celeritas/io/ImportOpticalMaterial.hh" +#include "celeritas/io/ImportOpticalModel.hh" + +#include "ValidationUtils.hh" + +namespace celeritas +{ +namespace optical +{ +namespace test +{ +//---------------------------------------------------------------------------// +// UNITS +//---------------------------------------------------------------------------// +struct Kelvin +{ + static CELER_CONSTEXPR_FUNCTION real_type value() { return units::kelvin; } + + static char const* label() { return "K"; } +}; + +struct MeterCubedPerMeV +{ + static CELER_CONSTEXPR_FUNCTION real_type value() + { + return ipow<3>(units::meter) / units::Mev::value(); + } + + static char const* label() { return "m^3/MeV"; } +}; + +//---------------------------------------------------------------------------// +// HELPER FUNCTIONS +//---------------------------------------------------------------------------// +/*! + * Helper function for converting hardcoded grids into \c ImportPhysicsVector. + * + * The grid energy is converted to units of MeV, while the values are converted + * to native units. + */ +template +ImportPhysicsVector +native_physics_vector_from(std::vector xs, std::vector ys) +{ + CELER_EXPECT(xs.size() == ys.size()); + ImportPhysicsVector v{ + ImportPhysicsVectorType::free, std::move(xs), std::move(ys)}; + for (double& x : v.x) + { + x = value_as(native_value_to( + native_value_from(Quantity(x)))); + } + + for (double& y : v.y) + { + y = native_value_from(Quantity(y)); + } + + return v; +} + +//---------------------------------------------------------------------------// +/*! + * Helper function for converting hardcoded tables (lists of grids) into + * \c ImportPhysicsVector. + */ +template +std::vector native_physics_table_from( + std::vector, std::vector>> data) +{ + std::vector table; + table.reserve(data.size()); + for (auto&& arrs : data) + { + table.push_back(native_physics_vector_from( + std::move(std::get<0>(arrs)), std::move(std::get<1>(arrs)))); + } + + return table; +} + +//---------------------------------------------------------------------------// +// OpticalMockTestBase +//---------------------------------------------------------------------------// +/*! + * Constructs optical material parameters from mock data. + */ +auto OpticalMockTestBase::build_optical_material() -> SPConstOpticalMaterial +{ + MaterialParams::Input input; + for (auto mat : this->imported_data().optical_materials) + { + input.properties.push_back(mat.properties); + } + + // Volume -> optical material mapping with some redundancies + for (auto opt_mat : range(8)) + { + input.volume_to_mat.push_back( + OpticalMaterialId(opt_mat % input.properties.size())); + } + + return std::make_shared(std::move(input)); +} + +//---------------------------------------------------------------------------// +/*! + * Constructs (core) material parameters from mock data. + * + * Only temperatures and optical material IDs are assigned meaningful values. + */ +auto OpticalMockTestBase::build_material() -> SPConstMaterial +{ + ::celeritas::MaterialParams::Input input; + + static constexpr auto material_temperatures + = native_array_from>( + 283.15, 300.0, 283.15, 200., 300.0); + + // Unused element - only to pass checks + input.elements.push_back(::celeritas::MaterialParams::ElementInput{ + AtomicNumber{1}, units::AmuMass{1}, {}, "fake"}); + + for (auto i : range(material_temperatures.size())) + { + // Only temperature is relevant information + input.materials.push_back(::celeritas::MaterialParams::MaterialInput{ + 0, + material_temperatures[i], + MatterState::solid, + {}, + std::to_string(i).c_str()}); + + // mock MaterialId == OpticalMaterialId + input.mat_to_optical.push_back(OpticalMaterialId(i)); + } + + return std::make_shared<::celeritas::MaterialParams const>( + std::move(input)); +} + +//---------------------------------------------------------------------------// +/*! + * Access mock imported data. + */ +ImportData const& OpticalMockTestBase::imported_data() const +{ + static ImportData data; + if (data.optical_materials.empty()) + { + this->build_import_data(data); + } + return data; +} + +//---------------------------------------------------------------------------// +/*! + * Create mock imported data in-place. + */ +void OpticalMockTestBase::build_import_data(ImportData& data) const +{ + // Build mock imported optical materials + { + data.optical_materials.resize(5); + + data.optical_materials[0].properties.refractive_index + = native_physics_vector_from( + {1.098177, 1.256172, 1.484130}, + {1.3235601610672, 1.3256740639273, 1.3280120256415}); + data.optical_materials[0].rayleigh.scale_factor = 1; + data.optical_materials[0].rayleigh.compressibility + = native_value_from(Quantity{7.658e-23}); + + data.optical_materials[1].properties.refractive_index + = native_physics_vector_from( + {1.098177, 1.256172, 1.484130}, + {1.3235601610672, 1.3256740639273, 1.3280120256415}); + data.optical_materials[1].rayleigh.scale_factor = 1.7; + data.optical_materials[1].rayleigh.compressibility + = native_value_from(Quantity{4.213e-24}); + + data.optical_materials[2].properties.refractive_index + = native_physics_vector_from( + {1.098177, 6.812319}, {1.3235601610672, 1.4679465862259}); + data.optical_materials[2].rayleigh.scale_factor = 1; + data.optical_materials[2].rayleigh.compressibility + = native_value_from(Quantity{7.658e-23}); + + data.optical_materials[3].properties.refractive_index + = native_physics_vector_from( + {1, 2, 5}, {1.3, 1.4, 1.5}); + data.optical_materials[3].rayleigh.scale_factor = 2; + data.optical_materials[3].rayleigh.compressibility + = native_value_from(Quantity{1e-20}); + + data.optical_materials[4].properties.refractive_index + = native_physics_vector_from( + {1.098177, 6.812319}, {1.3235601610672, 1.4679465862259}); + data.optical_materials[4].rayleigh.scale_factor = 1.7; + data.optical_materials[4].rayleigh.compressibility + = native_value_from(Quantity{4.213e-24}); + } + + // Build mock imported optical models + { + data.optical_models.resize(3); + + data.optical_models[0].model_class = ImportModelClass::absorption; + data.optical_models[0].mfp_table + = native_physics_table_from({ + {{1e-3, 1e-2}, {5.7, 9.3}}, + {{1e-2, 3e2}, {1.2, 10.7}}, + {{1e-2, 3e2}, {3.1, 5.4}}, + {{2e-3, 5e1, 1e2}, {0.1, 7.6, 12.5}}, + {{1e-3, 2e-3, 5e-1}, {1.3, 4.9, 9.4}}, + }); + + data.optical_models[1].model_class = ImportModelClass::rayleigh; + data.optical_models[1].mfp_table + = native_physics_table_from({ + {{1e-2, 3e2}, {5.7, 9.3}}, + {{1e-3, 1e-2}, {1.2, 10.7}}, + {{1e-3, 2e-3, 5e-1}, {0.1, 7.6, 12.5}}, + {{2e-3, 5e1, 1e2}, {0.1, 7.6, 12.5}}, + {{1e-3, 1e-2}, {3.1, 5.4}}, + }); + + data.optical_models[2].model_class = ImportModelClass::wls; + data.optical_models[2].mfp_table + = native_physics_table_from({ + {{1e-3, 2e-3, 5e-1}, {1.3, 4.9, 9.4}}, + {{1e-2, 3e2}, {5.7, 9.3}}, + {{1e-2, 3e2}, {1.2, 10.7}}, + {{2e-3, 5e1, 1e2}, {1.3, 4.9, 9.4}}, + {{1e-3, 2e-3, 5e-1}, {1.3, 4.9, 9.4}}, + }); + } +} + +//---------------------------------------------------------------------------// +/*! + * Get the imported optical model corresponding to the given \c + * ImportModelClass. + */ +ImportOpticalModel const& +OpticalMockTestBase::import_model_by_class(ImportModelClass imc) const +{ + switch (imc) + { + case ImportModelClass::absorption: + return this->imported_data().optical_models[0]; + case ImportModelClass::rayleigh: + return this->imported_data().optical_models[1]; + case ImportModelClass::wls: + return this->imported_data().optical_models[2]; + default: + CELER_ASSERT_UNREACHABLE(); + } +} + +//---------------------------------------------------------------------------// +} // namespace test +} // namespace optical +} // namespace celeritas diff --git a/test/celeritas/optical/OpticalMockTestBase.hh b/test/celeritas/optical/OpticalMockTestBase.hh new file mode 100644 index 0000000000..756fbcee12 --- /dev/null +++ b/test/celeritas/optical/OpticalMockTestBase.hh @@ -0,0 +1,77 @@ +//----------------------------------*-C++-*----------------------------------// +// Copyright 2024 UT-Battelle, LLC, and other Celeritas developers. +// See the top-level COPYRIGHT file for details. +// SPDX-License-Identifier: (Apache-2.0 OR MIT) +//---------------------------------------------------------------------------// +//! \file celeritas/optical/OpticalMockTestBase.hh +//---------------------------------------------------------------------------// +#pragma once + +#include "celeritas/io/ImportData.hh" +#include "celeritas/mat/MaterialParams.hh" +#include "celeritas/optical/MaterialParams.hh" + +#include "../GlobalTestBase.hh" + +namespace celeritas +{ +namespace optical +{ +namespace test +{ +using namespace ::celeritas::test; +//---------------------------------------------------------------------------// +/*! + * Class containing mock test data for optical physics. + */ +class OpticalMockTestBase : public GlobalTestBase +{ + public: + // Construct optical material parameters from mock data + SPConstOpticalMaterial build_optical_material() override; + + // Construct (core) material parameters from mock data + SPConstMaterial build_material() override; + + // Access mock imported data + ImportData const& imported_data() const; + + // Retrieve imported optical model data by class + ImportOpticalModel const& import_model_by_class(ImportModelClass) const; + + //! Number of mock optical materials + OpticalMaterialId::size_type num_optical_materials() const + { + return this->imported_data().optical_materials.size(); + } + + //!@{ + //! \name Unsupported params builders + SPConstGeo build_geometry() override { CELER_ASSERT_UNREACHABLE(); } + SPConstGeoMaterial build_geomaterial() override + { + CELER_ASSERT_UNREACHABLE(); + } + SPConstParticle build_particle() override { CELER_ASSERT_UNREACHABLE(); } + SPConstCutoff build_cutoff() override { CELER_ASSERT_UNREACHABLE(); } + SPConstPhysics build_physics() override { CELER_ASSERT_UNREACHABLE(); } + SPConstSim build_sim() override { CELER_ASSERT_UNREACHABLE(); } + SPConstTrackInit build_init() override { CELER_ASSERT_UNREACHABLE(); } + SPConstWentzelOKVI build_wentzel() override { CELER_ASSERT_UNREACHABLE(); } + SPConstAction build_along_step() override { CELER_ASSERT_UNREACHABLE(); } + SPConstCherenkov build_cherenkov() override { CELER_ASSERT_UNREACHABLE(); } + SPConstScintillation build_scintillation() override + { + CELER_ASSERT_UNREACHABLE(); + } + //!@} + + private: + // Construct mock import data in place + void build_import_data(ImportData&) const; +}; + +//---------------------------------------------------------------------------// +} // namespace test +} // namespace optical +} // namespace celeritas diff --git a/test/celeritas/optical/Rayleigh.test.cc b/test/celeritas/optical/Rayleigh.test.cc index 81ff352125..ef34328656 100644 --- a/test/celeritas/optical/Rayleigh.test.cc +++ b/test/celeritas/optical/Rayleigh.test.cc @@ -10,7 +10,8 @@ #include "celeritas/optical/model/RayleighModel.hh" #include "InteractorHostTestBase.hh" -#include "MockImportedData.hh" +#include "OpticalMockTestBase.hh" +#include "ValidationUtils.hh" #include "celeritas_test.hh" namespace celeritas @@ -42,7 +43,7 @@ class RayleighInteractorTest : public InteractorHostTestBase } }; -class RayleighModelTest : public MockImportedData +class RayleighModelTest : public OpticalMockTestBase { protected: void SetUp() override {} @@ -50,19 +51,17 @@ class RayleighModelTest : public MockImportedData //! Create Rayleigh model from mock data std::shared_ptr create_model() { - auto models = this->create_imported_models(); - import_model_id_ = models->builtin_model_id(ImportModelClass::rayleigh); + auto models = std::make_shared( + this->imported_data().optical_models); return std::make_shared(ActionId{0}, models); } - - ImportedModels::ImportedModelId import_model_id_; }; //---------------------------------------------------------------------------// // TESTS //---------------------------------------------------------------------------// // Basic tests for Rayleigh scattering interaction -TEST_F(RayleighInteractorTest, basic) +TEST_F(RayleighInteractorTest, TEST_IF_CELERITAS_DOUBLE(basic)) { int const num_samples = 4; @@ -103,7 +102,7 @@ TEST_F(RayleighInteractorTest, basic) //---------------------------------------------------------------------------// // Test statistical consistency over larger number of samples -TEST_F(RayleighInteractorTest, stress_test) +TEST_F(RayleighInteractorTest, TEST_IF_CELERITAS_DOUBLE(stress_test)) { int const num_samples = 1'000; @@ -157,17 +156,19 @@ TEST_F(RayleighModelTest, description) // Check Rayleigh model MFP tables match imported ones TEST_F(RayleighModelTest, interaction_mfp) { + OwningGridAccessor storage; + auto model = create_model(); - auto builder = this->create_mfp_builder(); + auto builder = storage.create_mfp_builder(); - for (auto mat : range(OpticalMaterialId(import_materials().size()))) + for (auto mat : range(OpticalMaterialId(this->num_optical_materials()))) { model->build_mfps(mat, builder); } - this->check_built_table_exact( - this->import_models()[import_model_id_.get()].mfp_table, - builder.grid_ids()); + EXPECT_TABLE_EQ( + this->import_model_by_class(ImportModelClass::rayleigh).mfp_table, + storage(builder.grid_ids())); } //---------------------------------------------------------------------------// diff --git a/test/celeritas/optical/RayleighMfpCalculator.test.cc b/test/celeritas/optical/RayleighMfpCalculator.test.cc index b56cfffcaa..ffbfa6fc02 100644 --- a/test/celeritas/optical/RayleighMfpCalculator.test.cc +++ b/test/celeritas/optical/RayleighMfpCalculator.test.cc @@ -9,7 +9,8 @@ #include "celeritas/mat/MaterialParams.hh" -#include "MockImportedData.hh" +#include "OpticalMockTestBase.hh" +#include "ValidationUtils.hh" #include "celeritas_test.hh" namespace celeritas @@ -23,43 +24,8 @@ using namespace ::celeritas::test; // TEST HARNESS //---------------------------------------------------------------------------// -class RayleighMfpCalculatorTest : public MockImportedData +class RayleighMfpCalculatorTest : public OpticalMockTestBase { - protected: - using SPConstCoreMaterials - = std::shared_ptr<::celeritas::MaterialParams const>; - - void SetUp() override {} - - SPConstCoreMaterials build_core_materials() const - { - ::celeritas::MaterialParams::Input input; - - static real_type const material_temperatures[] - = {283.15, 300.0, 283.15, 200, 300.0}; - - // Unused element - only to pass checks - input.elements.push_back(::celeritas::MaterialParams::ElementInput{ - AtomicNumber{1}, units::AmuMass{1}, {}, "fake"}); - - for (auto i : range(size_type{5})) - { - // Only temperature is relevant information - input.materials.push_back( - ::celeritas::MaterialParams::MaterialInput{ - 0, - material_temperatures[i] * units::kelvin, - MatterState::solid, - {}, - std::to_string(i).c_str()}); - - // mock MaterialId == OpticalMaterialId - input.mat_to_optical.push_back(OpticalMaterialId{i}); - } - - return std::make_shared<::celeritas::MaterialParams const>( - std::move(input)); - } }; //---------------------------------------------------------------------------// @@ -68,40 +34,38 @@ class RayleighMfpCalculatorTest : public MockImportedData // Check calculated MFPs match expected ones TEST_F(RayleighMfpCalculatorTest, mfp_table) { - static std::vector> expected_tables - = {{1189584.7068151, 682569.13017288, 343507.60086802}, - {12005096.767467, 6888377.4406869, 3466623.2384762}, - {1189584.7068151, 277.60444893823}, - {11510.805603078, 322.70360179716, 4.230373664558}, - {12005096.767467, 2801.539271218}}; - - auto core_materials = this->build_core_materials(); - - for (auto opt_mat : range(OpticalMaterialId(import_materials().size()))) + static constexpr auto expected_mfps = native_array_from( + // clang-format off + 1189584.7068151, 682569.13017288, 343507.60086802, 12005096.767467, + 6888377.4406869, 3466623.2384762, 1189584.7068151, 277.60444893823, + 11510.805603078, 322.70360179716, 4.230373664558, 12005096.767467, + 2801.539271218 + // clang-format on + ); + + auto core_materials = this->material(); + auto const& opt_materials = this->imported_data().optical_materials; + + std::vector mfps; + mfps.reserve(expected_mfps.size()); + + for (auto opt_mat : range(OpticalMaterialId(opt_materials.size()))) { - auto const& rayleigh = import_materials()[opt_mat.get()].rayleigh; + auto const& rayleigh = opt_materials[opt_mat.get()].rayleigh; RayleighMfpCalculator calc_mfp( - MaterialView(this->optical_materials()->host_ref(), opt_mat), + MaterialView(this->optical_material()->host_ref(), opt_mat), rayleigh, - ::celeritas::MaterialView(core_materials->host_ref(), - ::celeritas::MaterialId(opt_mat.get()))); + core_materials->get(::celeritas::MaterialId(opt_mat.get()))); auto energies = calc_mfp.grid().values(); - auto const& table = expected_tables[opt_mat.get()]; - - ASSERT_EQ(energies.size(), table.size()); - - std::vector expected_mfps(energies.size(), 0); - std::vector mfps(energies.size(), 0); for (auto i : range(energies.size())) { - expected_mfps[i] = table[i] * units::Centimeter::value(); - mfps[i] = calc_mfp(units::MevEnergy{energies[i]}); + mfps.push_back(calc_mfp(units::MevEnergy{energies[i]})); } - - EXPECT_VEC_SOFT_EQ(expected_mfps, mfps); } + + EXPECT_VEC_SOFT_EQ(expected_mfps, mfps); } //---------------------------------------------------------------------------// diff --git a/test/celeritas/optical/ValidationUtils.cc b/test/celeritas/optical/ValidationUtils.cc new file mode 100644 index 0000000000..a5e4f9b576 --- /dev/null +++ b/test/celeritas/optical/ValidationUtils.cc @@ -0,0 +1,82 @@ +//----------------------------------*-C++-*----------------------------------// +// Copyright 2024 UT-Battelle, LLC, and other Celeritas developers. +// See the top-level COPYRIGHT file for details. +// SPDX-License-Identifier: (Apache-2.0 OR MIT) +//---------------------------------------------------------------------------// +//! \file celeritas/optical/ValidationUtils.cc +//---------------------------------------------------------------------------// +#include "ValidationUtils.hh" + +namespace celeritas +{ +namespace optical +{ +namespace test +{ +using namespace ::celeritas::test; +//---------------------------------------------------------------------------// +/*! + * Construct validator for with the underlying storage. + */ +GridAccessor::GridAccessor(Items* reals, Items* grids) + : reals_(reals), grids_(grids) +{ + CELER_EXPECT(reals_); + CELER_EXPECT(grids_); +} + +//---------------------------------------------------------------------------// +/*! + * Retrieve a span of reals built on the storage. + */ +Span +GridAccessor::operator()(ItemRange const& real_ids) const +{ + CELER_EXPECT(real_ids.front() < real_ids.back()); + CELER_EXPECT(real_ids.back() < reals_->size()); + return (*reals_)[real_ids]; +} + +//---------------------------------------------------------------------------// +/*! + * Retrieve a table of grid views built on the storage. + * + * Each grid view is a pair of spans representing the grid and value of a + * \c GenericGridRecord. + */ +auto GridAccessor::operator()(ItemRange grid_ids) const + -> std::vector +{ + std::vector grids; + grids.reserve(grid_ids.size()); + + for (GridId grid_id : grid_ids) + { + CELER_EXPECT(grid_id < grids_->size()); + auto const& grid = (*grids_)[grid_id]; + CELER_EXPECT(grid); + grids.emplace_back((*this)(grid.grid), (*this)(grid.value)); + } + + return grids; +} + +//---------------------------------------------------------------------------// +/*! + * Construct an MFP builder with the underlying collections. + */ +MfpBuilder GridAccessor::create_mfp_builder() +{ + return MfpBuilder(reals_, grids_); +} + +//---------------------------------------------------------------------------// +/*! + * Construct with internal collections. + */ +OwningGridAccessor::OwningGridAccessor() : GridAccessor(&reals_, &grids_) {} + +//---------------------------------------------------------------------------// +} // namespace test +} // namespace optical +} // namespace celeritas diff --git a/test/celeritas/optical/ValidationUtils.hh b/test/celeritas/optical/ValidationUtils.hh new file mode 100644 index 0000000000..8babe5d958 --- /dev/null +++ b/test/celeritas/optical/ValidationUtils.hh @@ -0,0 +1,122 @@ +//----------------------------------*-C++-*----------------------------------// +// Copyright 2024 UT-Battelle, LLC, and other Celeritas developers. +// See the top-level COPYRIGHT file for details. +// SPDX-License-Identifier: (Apache-2.0 OR MIT) +//---------------------------------------------------------------------------// +//! \file celeritas/optical/ValidationUtils.hh +//---------------------------------------------------------------------------// +#pragma once + +#include +#include + +#include "corecel/cont/Array.hh" +#include "corecel/cont/Span.hh" +#include "corecel/data/Collection.hh" +#include "celeritas/UnitTypes.hh" +#include "celeritas/grid/GenericGridData.hh" +#include "celeritas/io/ImportPhysicsVector.hh" +#include "celeritas/optical/MfpBuilder.hh" + +#include "Test.hh" +#include "TestMacros.hh" + +#include "detail/ValidationUtilsImpl.hh" + +//! Equivalence macro for physics grids +#define EXPECT_GRID_EQ(expected, actual) \ + EXPECT_PRED_FORMAT2(::celeritas::testdetail::IsGridEq, expected, actual) + +//! Equivalence macro for physics tables (vectors of grids) +#define EXPECT_TABLE_EQ(expected, actual) \ + EXPECT_PRED_FORMAT2(::celeritas::testdetail::IsTableEq, expected, actual) + +namespace celeritas +{ +namespace optical +{ +namespace test +{ +using namespace ::celeritas::test; +//---------------------------------------------------------------------------// +/*! + * Helper function to annotate units of a hard-coded test data array. + * + * Converts the arguments supplied in units \c UnitType to native units. + */ +template +Array constexpr native_array_from(Args const&... args) +{ + return Array{ + native_value_from(UnitType(args))...}; +} + +//---------------------------------------------------------------------------// +/*! + * Helper function to annotate units of hard-coded test data. + * + * Same as \c native_array_from, except returns a vector. + */ +template +std::vector native_vector_from(Args const&... args) +{ + return std::vector{native_value_from(UnitType(args))...}; +} + +//---------------------------------------------------------------------------// +/*! + * Convenience class for accessing data built on the grid, and performing + * sanity checks on bounds. + */ +class GridAccessor +{ + public: + //!@{ + //! \name Type aliases + using Grid = GenericGridRecord; + using GridId = OpaqueId; + using ImportPhysicsTable = std::vector; + using GridView = std::tuple, Span>; + + template + using Items = Collection; + //!@} + + public: + // Construct accessor for underlying storage + GridAccessor(Items* reals, Items* grids); + + // Retrieve a table of grid views built on the storage + std::vector operator()(ItemRange grid_ids) const; + + // Retrieve a span of reals built on the storage + Span + operator()(ItemRange const& real_ids) const; + + // Construct an MFP builder with the underlying collections + MfpBuilder create_mfp_builder(); + + private: + Items* reals_; + Items* grids_; +}; + +//---------------------------------------------------------------------------// +/*! + * A \c GridAccessor that stores its own collections. + */ +class OwningGridAccessor : public GridAccessor +{ + public: + // Construct with internal collections + OwningGridAccessor(); + + private: + Items reals_; + Items grids_; +}; + +//---------------------------------------------------------------------------// +} // namespace test +} // namespace optical +} // namespace celeritas diff --git a/test/celeritas/optical/detail/ValidationUtilsImpl.hh b/test/celeritas/optical/detail/ValidationUtilsImpl.hh new file mode 100644 index 0000000000..ec1860151d --- /dev/null +++ b/test/celeritas/optical/detail/ValidationUtilsImpl.hh @@ -0,0 +1,165 @@ +//----------------------------------*-C++-*----------------------------------// +// Copyright 2024 UT-Battelle, LLC, and other Celeritas developers. +// See the top-level COPYRIGHT file for details. +// SPDX-License-Identifier: (Apache-2.0 OR MIT) +//---------------------------------------------------------------------------// +//! \file celeritas/optical/detail/ValidationUtilsImpl.hh +//---------------------------------------------------------------------------// +#pragma once + +#include +#include +#include +#include + +#include "corecel/cont/Array.hh" + +#include "TestMacros.hh" + +namespace celeritas +{ +namespace optical +{ +namespace test +{ +template +Array constexpr native_array_from(Args const&...); +} // namespace test +} // namespace optical + +namespace testdetail +{ +//---------------------------------------------------------------------------// +/*! + * Type traits for physics grids. + * + * Allows duck-typing for comparisons of physics grids that might be + * stored in different forms and floating point precisions. + */ +template +struct PhysicsGridTraits; + +//---------------------------------------------------------------------------// +//! Specialization for \c ImportPhysicsVector +template<> +struct PhysicsGridTraits +{ + using grid_type = ImportPhysicsVector; + + static constexpr std::vector const& grid(grid_type const& v) + { + return v.x; + } + static constexpr std::vector const& value(grid_type const& v) + { + return v.y; + } +}; + +//---------------------------------------------------------------------------// +//! Specialization for a tuple of containers +template +struct PhysicsGridTraits> +{ + using grid_type = std::tuple; + static constexpr T const& grid(grid_type const& v) + { + return std::get<0>(v); + } + static constexpr T const& value(grid_type const& v) + { + return std::get<1>(v); + } +}; + +//---------------------------------------------------------------------------// +/*! + * Compare to physics grids with exact equivalence. + */ +template +::testing::AssertionResult IsGridEq(char const* expected_expr, + char const* actual_expr, + GridTypeE const& expected, + GridTypeA const& actual) +{ + using EGT = PhysicsGridTraits; + using AGT = PhysicsGridTraits; + + // Compare grids (x values) + auto x_result = IsVecEq( + expected_expr, actual_expr, EGT::grid(expected), AGT::grid(actual)); + + // Compare values (y values) + auto y_result = IsVecEq( + expected_expr, actual_expr, EGT::value(expected), AGT::value(actual)); + + // Require both x and y successful to pass + ::testing::AssertionResult result(x_result && y_result); + if (!x_result) + { + result << "x values:\n" << x_result.message(); + } + if (!y_result) + { + result << "y values:\n" << y_result.message(); + } + + return result; +} + +//---------------------------------------------------------------------------// +/*! + * Compare physics tables with exact equivalence. + */ +template +::testing::AssertionResult IsTableEq(char const* expected_expr, + char const* actual_expr, + std::vector const& expected, + std::vector const& actual) +{ + // Check tables are equal size + if (expected.size() != actual.size()) + { + ::testing::AssertionResult failure = ::testing::AssertionFailure(); + + failure << " Size of: " << actual_expr + << "\n Actual: " << actual.size() + << "\nExpected: " << expected_expr + << ".size()\nWhich is: " << expected.size() << "\n"; + return failure; + } + + ::testing::AssertionResult result = ::testing::AssertionSuccess(); + + for (auto i : range(expected.size())) + { + // Extra formatting for table index + std::string index_expr = "[" + std::to_string(i) + "]"; + std::string expected_expr_i = expected_expr + index_expr; + std::string actual_expr_i = actual_expr + index_expr; + + // Compare table elements as physics grids + auto grid_result = IsGridEq(expected_expr_i.c_str(), + actual_expr_i.c_str(), + expected[i], + actual[i]); + + if (!grid_result) + { + // First failure needs to update result + if (result) + { + result = ::testing::AssertionFailure(); + } + + // Append failure message + result << grid_result.message(); + } + } + + return result; +} + +//---------------------------------------------------------------------------// +} // namespace testdetail +} // namespace celeritas From 4785d5908cfe9c530265391be1bbd79a9d3ef714 Mon Sep 17 00:00:00 2001 From: Julien Esseiva Date: Thu, 5 Dec 2024 10:09:37 -0800 Subject: [PATCH 12/15] Run clang-tidy on modified files only (#1531) * only run clang-tidy on modified files * fix fetch-depth * prefix base ref with remote * run clang-tidy in a separate step * fix * grep first * find the parent of 1st commit in that PR and use as base for diff * debug * more debug.. * disable exit on failure * try again * revert debug changes * fix var name * only follow merge parents * fix case when develop has been merged into current branch * list checked files * test script * Revert "test script" This reverts commit 170e2f130174fcac5c5a9bfa082a806ff9987ca3. --------- Co-authored-by: Seth R. Johnson --- .github/workflows/build-spack.yml | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-spack.yml b/.github/workflows/build-spack.yml index 477df483e9..2b81fda78f 100644 --- a/.github/workflows/build-spack.yml +++ b/.github/workflows/build-spack.yml @@ -63,7 +63,7 @@ jobs: - name: Check out Celeritas uses: actions/checkout@v4 with: - fetch-depth: 383 + fetch-depth: ${{format('{0}', matrix.special != 'clang-tidy' && 383 || 0)}} fetch-tags: true # to get version information - name: Setup Spack uses: spack/setup-spack@0ce61628ed8a32a5664cf01a0e0b5a4834a3b413 # 2024/03 @@ -135,15 +135,35 @@ jobs: test -n "${G4LEDATA}" fi cmake --preset=${CMAKE_PRESET} --log-level=VERBOSE + - name: Run incremental clang-tidy + id: clang-tidy + if: ${{matrix.special == 'clang-tidy'}} + env: + BASE_REF: "${{format('{0}', github.base_ref || 'develop')}}" + run: | + if [ "${{github.event_name}}" == "schedule" ]; then + echo "Full clang-tidy check on scheduled run." + ninja -k0 + exit $? + fi + BASE=$(git merge-base origin/${BASE_REF} HEAD) + ALL_FILES=$(git diff --name-only --diff-filter=ACM "$BASE" HEAD) + set +e + CC_FILES=$(grep -E '^(src|app|example)/.*\.cc$' - <<< "$ALL_FILES") + set -e + if [ -z "$CC_FILES" ]; then + echo "No files to run clang-tidy on." + exit 0 + fi + echo "Running clang-tidy on: $CC_FILES" + ${{env.CLANG_TIDY}} -p build $CC_FILES + exit $? - name: Build all id: build + if: ${{matrix.special != 'clang-tidy'}} working-directory: build run: | - if [ "${{matrix.special}}" == "clang-tidy" ]; then - ninja -k0 - else ninja -v -k0 - fi - name: Regenerate ROOT test data if: ${{matrix.geant == '11.0'}} working-directory: build From 12ee32c4f8d92a63c44cbf9eeaa6676478db0d95 Mon Sep 17 00:00:00 2001 From: Amanda Lund Date: Fri, 6 Dec 2024 10:16:21 -0600 Subject: [PATCH 13/15] Store a single process-integrated energy loss and range table per particle (#1536) --- src/celeritas/alongstep/detail/FluctELoss.hh | 5 +- src/celeritas/alongstep/detail/MeanELoss.hh | 5 +- src/celeritas/em/msc/detail/UrbanMscHelper.hh | 11 +- src/celeritas/phys/Model.hh | 1 - src/celeritas/phys/PhysicsData.hh | 29 +++-- src/celeritas/phys/PhysicsParams.cc | 101 +++++++++--------- src/celeritas/phys/PhysicsStepUtils.hh | 13 +-- src/celeritas/phys/PhysicsTrackView.hh | 95 +++++++++------- test/celeritas/em/MscTestBase.cc | 3 +- test/celeritas/phys/Physics.test.cc | 35 +++--- test/celeritas/phys/Physics.test.hh | 11 +- test/celeritas/phys/PhysicsStepUtils.test.cc | 6 +- 12 files changed, 152 insertions(+), 163 deletions(-) diff --git a/src/celeritas/alongstep/detail/FluctELoss.hh b/src/celeritas/alongstep/detail/FluctELoss.hh index f2c33b967a..3f1d5c9b12 100644 --- a/src/celeritas/alongstep/detail/FluctELoss.hh +++ b/src/celeritas/alongstep/detail/FluctELoss.hh @@ -81,9 +81,8 @@ CELER_FUNCTION bool FluctELoss::is_applicable(CoreTrackView const& track) const if (track.make_sim_view().status() == TrackStatus::errored) return false; - // Energy loss grid ID will be 'false' if inapplicable - auto ppid = track.make_physics_view().eloss_ppid(); - return static_cast(ppid); + // Energy loss grid ID is 'false' + return static_cast(track.make_physics_view().energy_loss_grid()); } //---------------------------------------------------------------------------// diff --git a/src/celeritas/alongstep/detail/MeanELoss.hh b/src/celeritas/alongstep/detail/MeanELoss.hh index ea9042ad0e..5b979686eb 100644 --- a/src/celeritas/alongstep/detail/MeanELoss.hh +++ b/src/celeritas/alongstep/detail/MeanELoss.hh @@ -53,9 +53,8 @@ CELER_FUNCTION bool MeanELoss::is_applicable(CoreTrackView const& track) const if (track.make_sim_view().status() == TrackStatus::errored) return false; - // Energy loss grid ID will be 'false' if inapplicable - auto ppid = track.make_physics_view().eloss_ppid(); - return static_cast(ppid); + // Energy loss grid ID is 'false' + return static_cast(track.make_physics_view().energy_loss_grid()); } //---------------------------------------------------------------------------// diff --git a/src/celeritas/em/msc/detail/UrbanMscHelper.hh b/src/celeritas/em/msc/detail/UrbanMscHelper.hh index d55ac470a9..8a442fc271 100644 --- a/src/celeritas/em/msc/detail/UrbanMscHelper.hh +++ b/src/celeritas/em/msc/detail/UrbanMscHelper.hh @@ -17,7 +17,6 @@ #include "celeritas/grid/EnergyLossCalculator.hh" #include "celeritas/grid/InverseRangeCalculator.hh" #include "celeritas/grid/RangeCalculator.hh" -#include "celeritas/grid/ValueGridType.hh" #include "celeritas/grid/XsCalculator.hh" #include "celeritas/phys/ParticleTrackView.hh" #include "celeritas/phys/PhysicsTrackView.hh" @@ -145,10 +144,8 @@ CELER_FUNCTION real_type UrbanMscHelper::calc_msc_mfp(Energy energy) const CELER_FUNCTION auto UrbanMscHelper::calc_inverse_range(real_type step) const -> Energy { - auto range_gid - = physics_.value_grid(ValueGridType::range, physics_.eloss_ppid()); - auto range_to_energy - = physics_.make_calculator(range_gid); + auto range_to_energy = physics_.make_calculator( + physics_.range_grid()); return range_to_energy(step); } @@ -172,11 +169,9 @@ UrbanMscHelper::calc_end_energy(real_type step) const -> Energy real_type range = physics_.dedx_range(); if (step <= range * shared_.params.dtrl()) { - auto eloss_gid = physics_.value_grid(ValueGridType::energy_loss, - physics_.eloss_ppid()); // Assume constant energy loss rate over the step real_type dedx = physics_.make_calculator( - eloss_gid)(particle_.energy()); + physics_.energy_loss_grid())(particle_.energy()); return particle_.energy() - Energy{step * dedx}; } diff --git a/src/celeritas/phys/Model.hh b/src/celeritas/phys/Model.hh index 90906a5561..6d3241b3b0 100644 --- a/src/celeritas/phys/Model.hh +++ b/src/celeritas/phys/Model.hh @@ -16,7 +16,6 @@ #include "celeritas/Types.hh" #include "celeritas/global/ActionInterface.hh" // IWYU pragma: export #include "celeritas/grid/ValueGridBuilder.hh" -#include "celeritas/grid/ValueGridType.hh" #include "Applicability.hh" // IWYU pragma: export diff --git a/src/celeritas/phys/PhysicsData.hh b/src/celeritas/phys/PhysicsData.hh index 797b0f3086..d761188a5b 100644 --- a/src/celeritas/phys/PhysicsData.hh +++ b/src/celeritas/phys/PhysicsData.hh @@ -16,7 +16,6 @@ #include "celeritas/em/data/AtomicRelaxationData.hh" #include "celeritas/em/data/EPlusGGData.hh" #include "celeritas/em/data/LivermorePEData.hh" -#include "celeritas/grid/ValueGridType.hh" #include "celeritas/grid/XsGridData.hh" #include "celeritas/neutron/data/NeutronElasticData.hh" @@ -118,22 +117,20 @@ struct IntegralXsProcess /*! * Processes for a single particle type. * - * Each index should be accessed with type ParticleProcessId. The "tables" are - * a fixed-size number of ItemRange references to ValueTables. The first index - * of the table (hard-coded) corresponds to ValueGridType; the second index is - * a ParticleProcessId. So the cross sections for ParticleProcessId{2} would - * be \code tables[ValueGridType::macro_xs][2] \endcode. This - * awkward access is encapsulated by the PhysicsTrackView. \c integral_xs will - * only be assigned if the integral approach is used and the particle has - * continuous-discrete processes. + * Each index should be accessed with type ParticleProcessId. \c macro_xs + * stores the cross section tables for each process, while \c energy_loss and + * \c range are the process-integrated dE/dx and range for the particle. \c + * integral_xs will only be assigned if the integral approach is used and the + * particle has continuous-discrete processes. */ struct ProcessGroup { ItemRange processes; //!< Processes that apply [ppid] - ValueGridArray> tables; //!< [vgt][ppid] - ItemRange integral_xs; //!< [ppid] ItemRange models; //!< Model applicability [ppid] - ParticleProcessId eloss_ppid{}; //!< Process with de/dx and range tables + ItemRange integral_xs; //!< [ppid] + ItemRange macro_xs; //!< [ppid] + ValueTableId energy_loss; //!< Process-integrated energy loss + ValueTableId range; //!< Process-integrated range bool has_at_rest{}; //!< Whether the particle type has an at-rest process //! True if assigned and valid @@ -306,16 +303,16 @@ struct PhysicsParamsScalars /*! * Persistent shared physics data. * - * This includes macroscopic cross section, energy loss, and range tables - * ordered by [particle][process][material][energy]. + * This includes macroscopic cross section tables ordered by + * [particle][process][material][energy] and process-integrated energy loss and + * range tables ordered by [particle][material][energy]. * * So the first applicable process (ProcessId{0}) for an arbitrary particle * (ParticleId{1}) in material 2 (MaterialId{2}) will have the following * ID and cross section grid: \code ProcessId proc_id = params.particle[1].processes[0]; const UniformGridData& grid - = - params.particle[1].table[int(ValueGridType::macro_xs)][0].material[2].log_energy; + = params.particle[1].macro_xs[0].material[2].log_energy; * \endcode */ template diff --git a/src/celeritas/phys/PhysicsParams.cc b/src/celeritas/phys/PhysicsParams.cc index c91a3d3ac9..4574aad00a 100644 --- a/src/celeritas/phys/PhysicsParams.cc +++ b/src/celeritas/phys/PhysicsParams.cc @@ -454,6 +454,7 @@ void PhysicsParams::build_xs(Options const& opts, using UPGridBuilder = Process::UPConstGridBuilder; using Energy = Applicability::Energy; + using VGT = ValueGridType; ValueGridInserter insert_grid(&data->reals, &data->value_grids); auto value_tables = make_builder(&data->value_tables); @@ -477,12 +478,11 @@ void PhysicsParams::build_xs(Options const& opts, = data->model_groups[process_groups.models]; CELER_ASSERT(processes.size() == model_groups.size()); - // Material-dependent physics tables, one per particle-process - ValueGridArray> temp_tables; - for (auto& vec : temp_tables) - { - vec.resize(processes.size()); - } + // Material-dependent physics tables, one cross section table per + // particle-process and one dedx/range table per particle + std::vector xs_table(processes.size()); + ValueTable eloss_table; + ValueTable range_table; // Processes with dE/dx and macro xs tables std::vector temp_integral_xs(processes.size()); @@ -501,11 +501,9 @@ void PhysicsParams::build_xs(Options const& opts, Process const& proc = *this->process(processes[pp_idx]); // Grid IDs for each grid type, each material - ValueGridArray> temp_grid_ids; - for (auto& vec : temp_grid_ids) - { - vec.resize(mats.size()); - } + std::vector xs_grid_ids(mats.size()); + std::vector eloss_grid_ids(mats.size()); + std::vector range_grid_ids(mats.size()); // Energy of maximum cross section for each material std::vector energy_max_xs; @@ -517,9 +515,9 @@ void PhysicsParams::build_xs(Options const& opts, } // Loop over materials - for (auto mat_id : range(MaterialId{mats.size()})) + for (auto mat_idx : range(MaterialId::size_type{mats.size()})) { - applic.material = mat_id; + applic.material = MaterialId(mat_idx); // Construct step limit builders auto builders = proc.step_limits(applic); @@ -532,11 +530,10 @@ void PhysicsParams::build_xs(Options const& opts, "have at least one)"); // Construct grids - for (auto vgt : range(ValueGridType::size_)) - { - temp_grid_ids[vgt][mat_id.get()] - = build_grid(builders[vgt]); - } + xs_grid_ids[mat_idx] = build_grid(builders[VGT::macro_xs]); + eloss_grid_ids[mat_idx] + = build_grid(builders[VGT::energy_loss]); + range_grid_ids[mat_idx] = build_grid(builders[VGT::range]); if (processes[pp_idx] == data->hardwired.positron_annihilation) { @@ -547,11 +544,10 @@ void PhysicsParams::build_xs(Options const& opts, { // Annihilation cross section is maximum at zero and // decreases with increasing energy - energy_max_xs[mat_id.get()] = 0; + energy_max_xs[mat_idx] = 0; } } - else if (auto grid_id - = temp_grid_ids[ValueGridType::macro_xs][mat_id.get()]) + else if (auto grid_id = xs_grid_ids[mat_idx]) { auto const& grid_data = data->value_grids[grid_id]; auto data_ref = make_const_ref(*data); @@ -579,37 +575,39 @@ void PhysicsParams::build_xs(Options const& opts, } } CELER_ASSERT(e_max > 0); - energy_max_xs[mat_id.get()] = e_max; + energy_max_xs[mat_idx] = e_max; } } - - // Index of the energy loss process that stores the de/dx and - // range tables - if (temp_grid_ids[ValueGridType::energy_loss][mat_id.get()] - && temp_grid_ids[ValueGridType::range][mat_id.get()]) - { - // Only one particle-process should have energy loss tables - CELER_ASSERT(!process_groups.eloss_ppid - || pp_idx == process_groups.eloss_ppid.get()); - process_groups.eloss_ppid = ParticleProcessId{pp_idx}; - } } - // Outer loop over grid types - for (auto vgt : range(ValueGridType::size_)) - { - if (!std::any_of(temp_grid_ids[vgt].begin(), - temp_grid_ids[vgt].end(), - [](ValueGridId id) { return bool(id); })) - { - continue; - } + // Check if any material has value grids + auto has_grids = [](std::vector const& vec_id) { + return std::any_of(vec_id.begin(), + vec_id.end(), + [](ValueGridId id) { return bool(id); }); + }; - // Construct value grid table - ValueTable& temp_table = temp_tables[vgt][pp_idx]; - temp_table.grids = value_grid_ids.insert_back( - temp_grid_ids[vgt].begin(), temp_grid_ids[vgt].end()); - CELER_ASSERT(temp_table.grids.size() == mats.size()); + // Construct value grid tables + if (has_grids(xs_grid_ids)) + { + xs_table[pp_idx].grids = value_grid_ids.insert_back( + xs_grid_ids.begin(), xs_grid_ids.end()); + CELER_ASSERT(xs_table[pp_idx].grids.size() == mats.size()); + } + if (has_grids(eloss_grid_ids)) + { + CELER_VALIDATE(!eloss_table && !range_table, + << "more than one process for particle ID " + << particle_id.get() + << " has energy loss tables"); + + CELER_ASSERT(has_grids(range_grid_ids)); + eloss_table.grids = value_grid_ids.insert_back( + eloss_grid_ids.begin(), eloss_grid_ids.end()); + range_table.grids = value_grid_ids.insert_back( + range_grid_ids.begin(), range_grid_ids.end()); + CELER_ASSERT(eloss_table.grids.size() == mats.size() + && range_table.grids.size() == mats.size()); } // Store the energies of the maximum cross sections @@ -627,11 +625,10 @@ void PhysicsParams::build_xs(Options const& opts, temp_integral_xs.begin(), temp_integral_xs.end()); // Construct value tables - for (auto vgt : range(ValueGridType::size_)) - { - process_groups.tables[vgt] = value_tables.insert_back( - temp_tables[vgt].begin(), temp_tables[vgt].end()); - } + process_groups.macro_xs + = value_tables.insert_back(xs_table.begin(), xs_table.end()); + process_groups.energy_loss = value_tables.push_back(eloss_table); + process_groups.range = value_tables.push_back(range_table); } } diff --git a/src/celeritas/phys/PhysicsStepUtils.hh b/src/celeritas/phys/PhysicsStepUtils.hh index 4120a955d9..ce7d583a1b 100644 --- a/src/celeritas/phys/PhysicsStepUtils.hh +++ b/src/celeritas/phys/PhysicsStepUtils.hh @@ -18,7 +18,6 @@ #include "celeritas/grid/InverseRangeCalculator.hh" #include "celeritas/grid/RangeCalculator.hh" #include "celeritas/grid/SplineXsCalculator.hh" -#include "celeritas/grid/ValueGridType.hh" #include "celeritas/grid/XsCalculator.hh" #include "celeritas/mat/MaterialTrackView.hh" #include "celeritas/random/Selector.hh" @@ -42,8 +41,6 @@ calc_physics_step_limit(MaterialTrackView const& material, { CELER_EXPECT(physics.has_interaction_mfp()); - using VGT = ValueGridType; - /*! \todo For particles with decay, macro XS calculation will incorporate * decay probability, dividing decay constant by speed to become 1/len to * compete with interactions. @@ -86,9 +83,8 @@ calc_physics_step_limit(MaterialTrackView const& material, else { limit.step = physics.interaction_mfp() / total_macro_xs; - if (auto ppid = physics.eloss_ppid()) + if (auto grid_id = physics.range_grid()) { - auto grid_id = physics.value_grid(VGT::range, ppid); auto calc_range = physics.make_calculator(grid_id); real_type range = calc_range(particle.energy()); // Save range for the current step and reuse it elsewhere @@ -180,20 +176,17 @@ calc_mean_energy_loss(ParticleTrackView const& particle, real_type step) { CELER_EXPECT(step > 0); - CELER_EXPECT(physics.eloss_ppid()); using Energy = ParticleTrackView::Energy; - using VGT = ValueGridType; static_assert(Energy::unit_type::value() == EnergyLossCalculator::Energy::unit_type::value(), "Incompatible energy types"); - auto ppid = physics.eloss_ppid(); Energy const pre_step_energy = particle.energy(); // Calculate the sum of energy loss rate over all processes. Energy eloss; { - auto grid_id = physics.value_grid(VGT::energy_loss, ppid); + auto grid_id = physics.energy_loss_grid(); CELER_ASSERT(grid_id); size_type order = physics.scalars().spline_eloss_order; @@ -218,7 +211,7 @@ calc_mean_energy_loss(ParticleTrackView const& particle, // approximation is probably wrong. Use the definition of the range as // the integral of 1/loss to back-calculate the actual energy loss // along the curve given the actual step. - auto grid_id = physics.value_grid(VGT::range, ppid); + auto grid_id = physics.range_grid(); CELER_ASSERT(grid_id); // Use the range limit stored from calc_physics_step_limit diff --git a/src/celeritas/phys/PhysicsTrackView.hh b/src/celeritas/phys/PhysicsTrackView.hh index cdec923bba..f1fb88deff 100644 --- a/src/celeritas/phys/PhysicsTrackView.hh +++ b/src/celeritas/phys/PhysicsTrackView.hh @@ -95,9 +95,14 @@ class PhysicsTrackView // Process ID for the given within-particle process index inline CELER_FUNCTION ProcessId process(ParticleProcessId) const; - // Get table, null if not present for this particle/material/type - inline CELER_FUNCTION ValueGridId value_grid(ValueGridType table, - ParticleProcessId) const; + // Get macro xs table, null if not present for this particle/material + inline CELER_FUNCTION ValueGridId macro_xs_grid(ParticleProcessId) const; + + // Get energy loss table, null if not present for this particle/material + inline CELER_FUNCTION ValueGridId energy_loss_grid() const; + + // Get range table, null if not present for this particle/material + inline CELER_FUNCTION ValueGridId range_grid() const; // Get data for processes that use the integral approach inline CELER_FUNCTION IntegralXsProcess const& @@ -164,9 +169,6 @@ class PhysicsTrackView inline CELER_FUNCTION ModelId hardwired_model(ParticleProcessId ppid, Energy energy) const; - // Particle-process ID of the process with the de/dx and range tables - inline CELER_FUNCTION ParticleProcessId eloss_ppid() const; - private: PhysicsParamsRef const& params_; PhysicsStateRef const& states_; @@ -179,6 +181,7 @@ class PhysicsTrackView CELER_FORCEINLINE_FUNCTION PhysicsTrackState& state(); CELER_FORCEINLINE_FUNCTION PhysicsTrackState const& state() const; CELER_FORCEINLINE_FUNCTION ProcessGroup const& process_group() const; + inline CELER_FUNCTION ValueGridId value_grid(ValueTableId) const; }; //---------------------------------------------------------------------------// @@ -333,35 +336,31 @@ CELER_FUNCTION ProcessId PhysicsTrackView::process(ParticleProcessId ppid) const //---------------------------------------------------------------------------// /*! - * Return value grid data for the given table type and process if available. - * - * If the result is not null, it can be used to instantiate a - * grid Calculator. - * - * If the result is null, it's likely because the process doesn't have the - * associated value (e.g. if the table type is "energy_loss" and the process is - * not a slowing-down process). + * Return macro xs value grid data for the given process if available. */ CELER_FUNCTION auto -PhysicsTrackView::value_grid(ValueGridType table_type, - ParticleProcessId ppid) const -> ValueGridId +PhysicsTrackView::macro_xs_grid(ParticleProcessId ppid) const -> ValueGridId { - CELER_EXPECT(int(table_type) < int(ValueGridType::size_)); CELER_EXPECT(ppid < this->num_particle_processes()); - ValueTableId table_id - = this->process_group().tables[table_type][ppid.get()]; - - CELER_ASSERT(table_id); - ValueTable const& table = params_.value_tables[table_id]; - if (!table) - return {}; // No table for this process + return this->value_grid(this->process_group().macro_xs[ppid.get()]); +} - CELER_EXPECT(material_ < table.grids.size()); - auto grid_id_ref = table.grids[material_.get()]; - if (!grid_id_ref) - return {}; // No table for this particular material +//---------------------------------------------------------------------------// +/*! + * Return the energy loss grid data if available. + */ +CELER_FUNCTION auto PhysicsTrackView::energy_loss_grid() const -> ValueGridId +{ + return this->value_grid(this->process_group().energy_loss); +} - return params_.value_grid_ids[grid_id_ref]; +//---------------------------------------------------------------------------// +/*! + * Return the range grid data if available. + */ +CELER_FUNCTION auto PhysicsTrackView::range_grid() const -> ValueGridId +{ + return this->value_grid(this->process_group().range); } //---------------------------------------------------------------------------// @@ -430,7 +429,7 @@ CELER_FUNCTION real_type PhysicsTrackView::calc_xs(ParticleProcessId ppid, result = calc_xs(energy); } } - else if (auto grid_id = this->value_grid(ValueGridType::macro_xs, ppid)) + else if (auto grid_id = this->macro_xs_grid(ppid)) { // Calculate cross section from the tabulated data auto calc_xs = this->make_calculator(grid_id); @@ -499,15 +498,6 @@ CELER_FUNCTION ModelId PhysicsTrackView::hardwired_model(ParticleProcessId ppid, return {}; } -//---------------------------------------------------------------------------// -/*! - * Particle-process ID of the process with the de/dx and range tables. - */ -CELER_FUNCTION ParticleProcessId PhysicsTrackView::eloss_ppid() const -{ - return this->process_group().eloss_ppid; -} - //---------------------------------------------------------------------------// /*! * Models that apply to the given process ID. @@ -695,6 +685,33 @@ CELER_FUNCTION T PhysicsTrackView::make_calculator(ValueGridId id, //---------------------------------------------------------------------------// // IMPLEMENTATION HELPER FUNCTIONS //---------------------------------------------------------------------------// +/*! + * Return value grid data for the given table ID if available. + * + * If the result is not null, it can be used to instantiate a + * grid Calculator. + * + * If the result is null, it's likely because the process doesn't have the + * associated value (e.g. if the table type is "energy_loss" and the process is + * not a slowing-down process). + */ +CELER_FUNCTION auto PhysicsTrackView::value_grid(ValueTableId table_id) const + -> ValueGridId +{ + CELER_EXPECT(table_id); + + ValueTable const& table = params_.value_tables[table_id]; + if (!table) + return {}; // No table for this process + + CELER_EXPECT(material_ < table.grids.size()); + auto grid_id_ref = table.grids[material_.get()]; + if (!grid_id_ref) + return {}; // No table for this particular material + + return params_.value_grid_ids[grid_id_ref]; +} + //! Get the thread-local state (mutable) CELER_FUNCTION PhysicsTrackState& PhysicsTrackView::state() { diff --git a/test/celeritas/em/MscTestBase.cc b/test/celeritas/em/MscTestBase.cc index d04b83adc8..eab460f15a 100644 --- a/test/celeritas/em/MscTestBase.cc +++ b/test/celeritas/em/MscTestBase.cc @@ -78,8 +78,7 @@ MscTestBase::make_phys_view(ParticleTrackView const& par, phys_view = PhysicsTrackInitializer{}; // Calculate and store the energy loss (dedx) range limit - auto ppid = phys_view.eloss_ppid(); - auto grid_id = phys_view.value_grid(ValueGridType::range, ppid); + auto grid_id = phys_view.range_grid(); auto calc_range = phys_view.make_calculator(grid_id); real_type range = calc_range(par.energy()); phys_view.dedx_range(range); diff --git a/test/celeritas/phys/Physics.test.cc b/test/celeritas/phys/Physics.test.cc index afea841c62..0ada6b34f6 100644 --- a/test/celeritas/phys/Physics.test.cc +++ b/test/celeritas/phys/Physics.test.cc @@ -142,7 +142,7 @@ TEST_F(PhysicsParamsTest, output) GTEST_SKIP() << "Test results are based on CGS units"; } EXPECT_JSON_EQ( - R"json({"_category":"internal","_label":"physics","models":{"label":["mock-model-1","mock-model-2","mock-model-3","mock-model-4","mock-model-5","mock-model-6","mock-model-7","mock-model-8","mock-model-9","mock-model-10","mock-model-11"],"process_id":[0,0,1,2,2,2,3,3,4,4,5]},"options":{"fixed_step_limiter":0.0,"linear_loss_limit":0.01,"lowest_electron_energy":[0.001,"MeV"],"max_step_over_range":0.2,"min_eprime_over_e":0.8,"min_range":0.1,"spline_eloss_order":1},"processes":{"label":["scattering","absorption","purrs","hisses","meows","barks"]},"sizes":{"integral_xs":8,"model_groups":8,"model_ids":11,"process_groups":5,"process_ids":8,"reals":257,"value_grid_ids":89,"value_grids":89,"value_tables":35}})json", + R"json({"_category":"internal","_label":"physics","models":{"label":["mock-model-1","mock-model-2","mock-model-3","mock-model-4","mock-model-5","mock-model-6","mock-model-7","mock-model-8","mock-model-9","mock-model-10","mock-model-11"],"process_id":[0,0,1,2,2,2,3,3,4,4,5]},"options":{"fixed_step_limiter":0.0,"linear_loss_limit":0.01,"lowest_electron_energy":[0.001,"MeV"],"max_step_over_range":0.2,"min_eprime_over_e":0.8,"min_range":0.1,"spline_eloss_order":1},"processes":{"label":["scattering","absorption","purrs","hisses","meows","barks"]},"sizes":{"integral_xs":8,"model_groups":8,"model_ids":11,"process_groups":5,"process_ids":8,"reals":257,"value_grid_ids":89,"value_grids":89,"value_tables":29}})json", to_string(out)); } @@ -413,6 +413,10 @@ TEST_F(PhysicsTrackViewHostTest, value_grids) { std::vector grid_ids; + auto id_to_int = [](ValueGridId vgid) { + return vgid ? static_cast(vgid.unchecked_get()) : -1; + }; + for (char const* particle : {"gamma", "celeriton", "anti-celeriton"}) { for (auto mat_id : range(MaterialId{this->material()->size()})) @@ -423,23 +427,20 @@ TEST_F(PhysicsTrackViewHostTest, value_grids) for (auto pp_id : range(ParticleProcessId{phys.num_particle_processes()})) { - for (ValueGridType vgt : range(ValueGridType::size_)) - { - auto id = phys.value_grid(vgt, pp_id); - grid_ids.push_back(id ? static_cast(id.get()) : -1); - } + grid_ids.push_back(id_to_int(phys.macro_xs_grid(pp_id))); } + grid_ids.push_back(id_to_int(phys.energy_loss_grid())); + grid_ids.push_back(id_to_int(phys.range_grid())); } } // Grid IDs should be unique if they exist. Gammas should have fewer // because there aren't any slowing down/range limiters. - static int const expected_grid_ids[] - = {0, -1, -1, 4, -1, -1, 1, -1, -1, 5, -1, -1, 2, -1, -1, 6, -1, - -1, 3, -1, -1, 7, -1, -1, 8, -1, -1, 12, 13, 14, 24, -1, -1, 9, - -1, -1, 15, 16, 17, 25, -1, -1, 10, -1, -1, 18, 19, 20, 26, -1, -1, - 11, -1, -1, 21, 22, 23, 27, -1, -1, 28, 29, 30, 40, -1, -1, 31, 32, - 33, 41, -1, -1, 34, 35, 36, 42, -1, -1, 37, 38, 39, 43, -1, -1}; + static int const expected_grid_ids[] = { + 0, 4, -1, -1, 1, 5, -1, -1, 2, 6, -1, -1, 3, 7, -1, -1, 8, 12, + 24, 13, 14, 9, 15, 25, 16, 17, 10, 18, 26, 19, 20, 11, 21, 27, 22, 23, + 28, 40, 29, 30, 31, 41, 32, 33, 34, 42, 35, 36, 37, 43, 38, 39, + }; EXPECT_VEC_EQ(expected_grid_ids, grid_ids); } @@ -455,7 +456,7 @@ TEST_F(PhysicsTrackViewHostTest, calc_xs) PhysicsTrackView const phys = this->make_track_view(particle, mat_id); auto scat_ppid = this->find_ppid(phys, "scattering"); - auto id = phys.value_grid(ValueGridType::macro_xs, scat_ppid); + auto id = phys.macro_xs_grid(scat_ppid); ASSERT_TRUE(id); auto calc_xs = phys.make_calculator(id); xs.push_back(to_inv_cm(calc_xs(MevEnergy{1.0}))); @@ -482,14 +483,12 @@ TEST_F(PhysicsTrackViewHostTest, calc_eloss_range) { PhysicsTrackView const phys = this->make_track_view(particle, MaterialId{0}); - auto ppid = phys.eloss_ppid(); - ASSERT_TRUE(ppid); - auto eloss_id = phys.value_grid(ValueGridType::energy_loss, ppid); + auto eloss_id = phys.energy_loss_grid(); ASSERT_TRUE(eloss_id); auto calc_eloss = phys.make_calculator(eloss_id); - auto range_id = phys.value_grid(ValueGridType::range, ppid); + auto range_id = phys.range_grid(); ASSERT_TRUE(range_id); auto calc_range = phys.make_calculator(range_id); for (real_type energy : {1e-6, 0.01, 1.0, 1e2}) @@ -661,7 +660,7 @@ TEST_F(PhysicsTrackViewHostTest, calc_spline_xs) PhysicsTrackView const phys = this->make_track_view(particle, mat_id); auto scat_ppid = this->find_ppid(phys, "scattering"); - auto id = phys.value_grid(ValueGridType::macro_xs, scat_ppid); + auto id = phys.macro_xs_grid(scat_ppid); ASSERT_TRUE(id); auto calc_xs = phys.make_calculator(id, 2); xs.push_back(to_inv_cm(calc_xs(MevEnergy{1.0}))); diff --git a/test/celeritas/phys/Physics.test.hh b/test/celeritas/phys/Physics.test.hh index 0f7d0032f0..e68faa3d97 100644 --- a/test/celeritas/phys/Physics.test.hh +++ b/test/celeritas/phys/Physics.test.hh @@ -61,7 +61,7 @@ inline CELER_FUNCTION real_type calc_step(PhysicsTrackView& phys, for (auto ppid : range(ParticleProcessId{phys.num_particle_processes()})) { real_type process_xs = 0; - if (auto id = phys.value_grid(ValueGridType::macro_xs, ppid)) + if (auto id = phys.macro_xs_grid(ppid)) { auto calc_xs = phys.make_calculator(id); process_xs = calc_xs(energy); @@ -82,13 +82,10 @@ inline CELER_FUNCTION real_type calc_step(PhysicsTrackView& phys, // Calc minimum range auto const inf = numeric_limits::infinity(); real_type step = inf; - for (auto ppid : range(ParticleProcessId{phys.num_particle_processes()})) + if (auto id = phys.range_grid()) { - if (auto id = phys.value_grid(ValueGridType::range, ppid)) - { - auto calc_range = phys.make_calculator(id); - step = min(step, calc_range(energy)); - } + auto calc_range = phys.make_calculator(id); + step = min(step, calc_range(energy)); } if (step != inf) { diff --git a/test/celeritas/phys/PhysicsStepUtils.test.cc b/test/celeritas/phys/PhysicsStepUtils.test.cc index 188c70dbff..2484376bb3 100644 --- a/test/celeritas/phys/PhysicsStepUtils.test.cc +++ b/test/celeritas/phys/PhysicsStepUtils.test.cc @@ -227,8 +227,7 @@ TEST_F(PhysicsStepUtilsTest, calc_mean_energy_loss) // input: cm; output: MeV auto calc_eloss = [&](PhysicsTrackView& phys, real_type step) -> real_type { // Calculate and store the energy loss range to PhysicsTrackView - auto ppid = phys.eloss_ppid(); - auto grid_id = phys.value_grid(ValueGridType::range, ppid); + auto grid_id = phys.range_grid(); auto calc_range = phys.make_calculator(grid_id); real_type range = calc_range(particle.energy()); phys.dedx_range(range); @@ -481,8 +480,7 @@ TEST_F(SplinePhysicsStepUtilsTest, calc_mean_energy_loss) // input: cm; output: MeV auto calc_eloss = [&](PhysicsTrackView& phys, real_type step) -> real_type { // Calculate and store the energy loss range to PhysicsTrackView - auto ppid = phys.eloss_ppid(); - auto grid_id = phys.value_grid(ValueGridType::range, ppid); + auto grid_id = phys.range_grid(); auto calc_range = phys.make_calculator(grid_id); real_type range = calc_range(particle.energy()); phys.dedx_range(range); From 9ddda63a45bdc659e92818b0b70a2cfd4dafc128 Mon Sep 17 00:00:00 2001 From: "Seth R. Johnson" Date: Fri, 6 Dec 2024 12:51:20 -0500 Subject: [PATCH 14/15] Add piecewise integrator and CDF utilities (#1537) * Add cdf utils * Use material view to construct cherenkov data * Use segment integrator for cherenkov * Use integrator for WLS * Store WLS CDF as energy -> CDF rather than inverse * Address review feedback --- app/celer-sim/Runner.cc | 2 +- src/celeritas/grid/GenericCalculator.hh | 2 + src/celeritas/optical/CherenkovParams.cc | 45 ++++---- src/celeritas/optical/CherenkovParams.hh | 8 +- src/celeritas/optical/WavelengthShiftData.hh | 4 +- .../optical/WavelengthShiftParams.cc | 33 ++---- .../interactor/WavelengthShiftInteractor.hh | 12 +- src/corecel/math/CdfUtils.hh | 109 ++++++++++++++++++ test/celeritas/ImportedDataTestBase.cc | 3 +- test/celeritas/optical/Cherenkov.test.cc | 2 +- .../celeritas/optical/WavelengthShift.test.cc | 17 ++- test/corecel/CMakeLists.txt | 1 + test/corecel/math/CdfUtils.test.cc | 87 ++++++++++++++ 13 files changed, 251 insertions(+), 74 deletions(-) create mode 100644 src/corecel/math/CdfUtils.hh create mode 100644 test/corecel/math/CdfUtils.test.cc diff --git a/app/celer-sim/Runner.cc b/app/celer-sim/Runner.cc index 13b5296598..239350bba8 100644 --- a/app/celer-sim/Runner.cc +++ b/app/celer-sim/Runner.cc @@ -604,7 +604,7 @@ void Runner::build_optical_collector(RunnerInput const& inp, OpticalCollector::Input oc_inp; oc_inp.material = MaterialParams::from_import( imported, *core_params_->geomaterial(), *core_params_->material()); - oc_inp.cherenkov = std::make_shared(oc_inp.material); + oc_inp.cherenkov = std::make_shared(*oc_inp.material); oc_inp.scintillation = ScintillationParams::from_import(imported, core_params_->particle()); oc_inp.num_track_slots = ceil_div(inp.optical.num_track_slots, num_streams); diff --git a/src/celeritas/grid/GenericCalculator.hh b/src/celeritas/grid/GenericCalculator.hh index 64b8a30d15..9b23f2c1d1 100644 --- a/src/celeritas/grid/GenericCalculator.hh +++ b/src/celeritas/grid/GenericCalculator.hh @@ -24,6 +24,8 @@ namespace celeritas * Find and interpolate real numbers on a nonuniform grid. * * The end points of the grid are extrapolated outward as constant values. + * + * \todo Rename NonuniformGridCalculator? Template on value type and/or units? */ class GenericCalculator { diff --git a/src/celeritas/optical/CherenkovParams.cc b/src/celeritas/optical/CherenkovParams.cc index 39f2b655f3..725d81319b 100644 --- a/src/celeritas/optical/CherenkovParams.cc +++ b/src/celeritas/optical/CherenkovParams.cc @@ -14,11 +14,14 @@ #include "corecel/data/CollectionBuilder.hh" #include "corecel/data/DedupeCollectionBuilder.hh" #include "corecel/math/Algorithms.hh" +#include "corecel/math/CdfUtils.hh" #include "celeritas/Quantities.hh" #include "celeritas/Types.hh" +#include "celeritas/grid/GenericCalculator.hh" #include "celeritas/grid/GenericGridInserter.hh" #include "MaterialParams.hh" +#include "MaterialView.hh" namespace celeritas { @@ -28,41 +31,37 @@ namespace optical /*! * Construct with optical property data. */ -CherenkovParams::CherenkovParams(SPConstMaterial material) +CherenkovParams::CherenkovParams(MaterialParams const& mats) { - CELER_EXPECT(material); - auto const& host_ref = material->host_ref(); + SegmentIntegrator integrate_rindex{TrapezoidSegmentIntegrator{}}; HostVal data; GenericGridInserter insert_angle_integral(&data.reals, &data.angle_integral); - - for (auto mat_id : - range(OpticalMaterialId(host_ref.refractive_index.size()))) + for (auto mat_id : range(OpticalMaterialId(mats.num_materials()))) { - auto const& ri_grid = host_ref.refractive_index[mat_id]; - CELER_ASSERT(ri_grid); + GenericCalculator refractive_index + = MaterialView{mats.host_ref(), mat_id} + .make_refractive_index_calculator(); + Span energy = refractive_index.grid().values(); - // Calculate the Cherenkov angle integral - auto const&& refractive_index = host_ref.reals[ri_grid.value]; - auto const&& energy = host_ref.reals[ri_grid.grid]; - std::vector integral(energy.size()); - for (size_type i = 1; i < energy.size(); ++i) + // Calculate 1/n^2 on all grid points + std::vector ri_inv_sq(energy.size()); + for (auto i : range(ri_inv_sq.size())) { - // TODO: use trapezoidal integrator helper class - integral[i] = integral[i - 1] - + real_type(0.5) * (energy[i] - energy[i - 1]) - * (1 / ipow<2>(refractive_index[i - 1]) - + 1 / ipow<2>(refractive_index[i])); + ri_inv_sq[i] = 1 / ipow<2>(refractive_index[i]); } - insert_angle_integral(make_span(energy), make_span(integral)); + // Integrate + std::vector integral(energy.size()); + integrate_rindex(energy, + Span(make_span(ri_inv_sq)), + make_span(integral)); + insert_angle_integral(energy, make_span(integral)); } - CELER_ASSERT(data.angle_integral.size() - == host_ref.refractive_index.size()); - + CELER_ASSERT(data.angle_integral.size() == mats.num_materials()); data_ = CollectionMirror{std::move(data)}; - CELER_ENSURE(data_ || host_ref.refractive_index.empty()); + CELER_ENSURE(data_); } //---------------------------------------------------------------------------// diff --git a/src/celeritas/optical/CherenkovParams.hh b/src/celeritas/optical/CherenkovParams.hh index b73ee5c2d9..646c9e8f02 100644 --- a/src/celeritas/optical/CherenkovParams.hh +++ b/src/celeritas/optical/CherenkovParams.hh @@ -25,15 +25,9 @@ class MaterialParams; */ class CherenkovParams final : public ParamsDataInterface { - public: - //!@{ - //! \name Type aliases - using SPConstMaterial = std::shared_ptr; - //!@} - public: // Construct with optical property data - explicit CherenkovParams(SPConstMaterial material); + explicit CherenkovParams(MaterialParams const& material); //! Access physics material on the host HostRef const& host_ref() const final { return data_.host_ref(); } diff --git a/src/celeritas/optical/WavelengthShiftData.hh b/src/celeritas/optical/WavelengthShiftData.hh index 1fa2fab6d6..53a8d1768d 100644 --- a/src/celeritas/optical/WavelengthShiftData.hh +++ b/src/celeritas/optical/WavelengthShiftData.hh @@ -49,7 +49,7 @@ struct WavelengthShiftData OpticalMaterialItems wls_record; - // Grid energy tabulated as a function of the cumulative probability. + // Cumulative probability of emission as a function of energy OpticalMaterialItems energy_cdf; // Backend data @@ -60,7 +60,7 @@ struct WavelengthShiftData //! Whether all data are assigned and valid explicit CELER_FUNCTION operator bool() const { - return !wls_record.empty() && !energy_cdf.empty() && !reals.empty(); + return !wls_record.empty() && !energy_cdf.empty(); } //! Assign from another set of data diff --git a/src/celeritas/optical/WavelengthShiftParams.cc b/src/celeritas/optical/WavelengthShiftParams.cc index d8a01eb1ce..e38062b413 100644 --- a/src/celeritas/optical/WavelengthShiftParams.cc +++ b/src/celeritas/optical/WavelengthShiftParams.cc @@ -10,6 +10,7 @@ #include #include "corecel/data/CollectionBuilder.hh" +#include "corecel/math/CdfUtils.hh" #include "celeritas/Types.hh" #include "celeritas/grid/GenericGridBuilder.hh" #include "celeritas/grid/GenericGridInserter.hh" @@ -52,8 +53,10 @@ WavelengthShiftParams::from_import(ImportData const& data) WavelengthShiftParams::WavelengthShiftParams(Input const& input) { CELER_EXPECT(input.data.size() > 0); - HostVal data; + SegmentIntegrator integrate_emission{TrapezoidSegmentIntegrator{}}; + + HostVal data; CollectionBuilder wls_record{&data.wls_record}; GenericGridInserter insert_energy_cdf(&data.reals, &data.energy_cdf); for (auto const& wls : input.data) @@ -73,28 +76,14 @@ WavelengthShiftParams::WavelengthShiftParams(Input const& input) wls_record.push_back(record); // Calculate the WLS cumulative probability of the emission spectrum - // Store WLS component tabulated as a function of photon energy - auto const& comp_vec = wls.component; - std::vector cdf(comp_vec.x.size()); - - CELER_ASSERT(comp_vec.y[0] > 0); - // The value of cdf at the low edge is zero by default - cdf[0] = 0; - for (size_type i = 1; i < comp_vec.x.size(); ++i) - { - // TODO: use trapezoidal integrator helper class - cdf[i] = cdf[i - 1] - + 0.5 * (comp_vec.x[i] - comp_vec.x[i - 1]) - * (comp_vec.y[i] + comp_vec.y[i - 1]); - } + std::vector cdf(wls.component.x.size()); + integrate_emission(make_span(wls.component.x), + make_span(wls.component.y), + make_span(cdf)); + normalize_cdf(make_span(cdf)); - // Normalize for the cdf probability - for (size_type i = 1; i < comp_vec.x.size(); ++i) - { - cdf[i] = cdf[i] / cdf.back(); - } - // Note that energy and cdf are swapped for the inverse sampling - insert_energy_cdf(make_span(cdf), make_span(comp_vec.x)); + // Insert energy -> CDF grid + insert_energy_cdf(make_span(wls.component.x), make_span(cdf)); } CELER_ASSERT(data.energy_cdf.size() == input.data.size()); CELER_ASSERT(data.wls_record.size() == data.energy_cdf.size()); diff --git a/src/celeritas/optical/interactor/WavelengthShiftInteractor.hh b/src/celeritas/optical/interactor/WavelengthShiftInteractor.hh index da7849d8ed..6524790191 100644 --- a/src/celeritas/optical/interactor/WavelengthShiftInteractor.hh +++ b/src/celeritas/optical/interactor/WavelengthShiftInteractor.hh @@ -74,7 +74,6 @@ class WavelengthShiftInteractor PoissonDistribution sample_num_photons_; ExponentialDistribution sample_time_; // Grid calculators - GenericCalculator calc_energy_; GenericCalculator calc_cdf_; // Allocate space for secondary particles SecondaryAllocator& allocate_; @@ -95,12 +94,10 @@ WavelengthShiftInteractor::WavelengthShiftInteractor( : inc_energy_(particle.energy()) , sample_num_photons_(shared.wls_record[mat_id].mean_num_photons) , sample_time_(real_type{1} / shared.wls_record[mat_id].time_constant) - , calc_energy_(shared.energy_cdf[mat_id], shared.reals) - , calc_cdf_(GenericCalculator::from_inverse(shared.energy_cdf[mat_id], - shared.reals)) + , calc_cdf_(shared.energy_cdf[mat_id], shared.reals) , allocate_(allocate) { - CELER_EXPECT(inc_energy_.value() > calc_energy_(0)); + CELER_EXPECT(inc_energy_.value() > calc_cdf_.grid().front()); } //---------------------------------------------------------------------------// @@ -140,18 +137,19 @@ CELER_FUNCTION Interaction WavelengthShiftInteractor::operator()(Engine& rng) result.secondaries = {secondaries, num_photons}; IsotropicDistribution sample_direction{}; + GenericCalculator calc_energy = calc_cdf_.make_inverse(); for (size_type i : range(num_photons)) { // Sample the emitted energy from the inverse cumulative distribution // TODO: add CDF sampler; see // https://github.com/celeritas-project/celeritas/pull/1507/files#r1844973621 - real_type energy = calc_energy_(generate_canonical(rng)); + real_type energy = calc_energy(generate_canonical(rng)); if (CELER_UNLIKELY(energy > inc_energy_.value())) { // Sample a restricted energy below the incident photon energy real_type cdf_max = calc_cdf_(inc_energy_.value()); UniformRealDistribution sample_cdf(0, cdf_max); - energy = calc_energy_(sample_cdf(rng)); + energy = calc_energy(sample_cdf(rng)); } CELER_ENSURE(energy < inc_energy_.value()); secondaries[i].energy = Energy{energy}; diff --git a/src/corecel/math/CdfUtils.hh b/src/corecel/math/CdfUtils.hh new file mode 100644 index 0000000000..7b0d6902c4 --- /dev/null +++ b/src/corecel/math/CdfUtils.hh @@ -0,0 +1,109 @@ +//----------------------------------*-C++-*----------------------------------// +// Copyright 2024 UT-Battelle, LLC, and other Celeritas developers. +// See the top-level COPYRIGHT file for details. +// SPDX-License-Identifier: (Apache-2.0 OR MIT) +//---------------------------------------------------------------------------// +//! \file corecel/math/CdfUtils.hh +//---------------------------------------------------------------------------// +#pragma once + +#include + +#include "corecel/cont/Array.hh" +#include "corecel/cont/Range.hh" +#include "corecel/cont/Span.hh" + +namespace celeritas +{ +//---------------------------------------------------------------------------// +/*! + * Calculate the integral of a piecewise rectangular function. + * + * The value at the left point is taken for the interval. + */ +struct PostRectangleSegmentIntegrator +{ + template + T operator()(Array lo, Array hi) const + { + return (hi[0] - lo[0]) * lo[1]; + } +}; + +//---------------------------------------------------------------------------// +/*! + * Calculate the integral of a piecewise linear function. + */ +struct TrapezoidSegmentIntegrator +{ + template + T operator()(Array lo, Array hi) const + { + return T(0.5) * (hi[0] - lo[0]) * (hi[1] + lo[1]); + } +}; + +//---------------------------------------------------------------------------// +/*! + * Integrate a piecewise function. + * + * To construct a CDF, `init` should be zero, and the destination should be + * normalized by its final value afterward. + */ +template +class SegmentIntegrator +{ + public: + //! Construct with integrator + explicit SegmentIntegrator(I&& integrate) + : integrate_{std::forward(integrate)} + { + } + + //! Integrate a function + template + void operator()(Span x, + Span f, + Span dst, + T init = {}) + { + CELER_EXPECT(x.size() == f.size()); + CELER_EXPECT(x.size() == dst.size()); + + using Array2 = Array; + + Array2 prev{x[0], f[0]}; + dst[0] = init; + for (auto i : range(std::size_t{1}, x.size())) + { + Array2 cur{x[i], f[i]}; + init += integrate_(prev, cur); + dst[i] = init; + prev = cur; + } + } + + private: + I integrate_; +}; + +//---------------------------------------------------------------------------// +/*! + * Normalize a vector by the final value and check for monotonicity. + */ +template +inline void normalize_cdf(Span x) +{ + CELER_EXPECT(!x.empty()); + CELER_EXPECT(x.back() > 0); + T norm{1 / x.back()}; + for (auto i : range(x.size() - 1)) + { + CELER_ASSERT(x[i + 1] >= x[i]); + x[i] *= norm; + } + x.back() = 1; +} + +//---------------------------------------------------------------------------// +} // namespace celeritas diff --git a/test/celeritas/ImportedDataTestBase.cc b/test/celeritas/ImportedDataTestBase.cc index 7169977ff2..ad1cde6682 100644 --- a/test/celeritas/ImportedDataTestBase.cc +++ b/test/celeritas/ImportedDataTestBase.cc @@ -135,7 +135,8 @@ auto ImportedDataTestBase::build_physics() -> SPConstPhysics //---------------------------------------------------------------------------// auto ImportedDataTestBase::build_cherenkov() -> SPConstCherenkov { - return std::make_shared(this->optical_material()); + return std::make_shared( + *this->optical_material()); } //---------------------------------------------------------------------------// diff --git a/test/celeritas/optical/Cherenkov.test.cc b/test/celeritas/optical/Cherenkov.test.cc index ca56813a85..71c66a23ee 100644 --- a/test/celeritas/optical/Cherenkov.test.cc +++ b/test/celeritas/optical/Cherenkov.test.cc @@ -149,7 +149,7 @@ class CherenkovTest : public ::celeritas::test::OpticalTestBase material = std::make_shared(std::move(input)); // Build Cherenkov data - params = std::make_shared(material); + params = std::make_shared(*material); } std::shared_ptr material; diff --git a/test/celeritas/optical/WavelengthShift.test.cc b/test/celeritas/optical/WavelengthShift.test.cc index 116237a660..5ffe73b956 100644 --- a/test/celeritas/optical/WavelengthShift.test.cc +++ b/test/celeritas/optical/WavelengthShift.test.cc @@ -86,21 +86,18 @@ TEST_F(WavelengthShiftTest, data) EXPECT_SOFT_EQ(1 * units::nanosecond, wls_record.time_constant); // Test the vector property (emission spectrum) of WLS - auto const& grid = data_.energy_cdf[material_id_]; - EXPECT_TRUE(grid); - auto const& cdf = data_.reals[grid.grid]; - EXPECT_EQ(5, cdf.size()); - EXPECT_SOFT_EQ(0, cdf.front()); - EXPECT_SOFT_EQ(1, cdf.back()); - - auto const& energy = data_.reals[grid.value]; + // Test the energy range and spectrum of emitted photons + GenericCalculator calc_cdf(data_.energy_cdf[material_id_], data_.reals); + auto const& energy = calc_cdf.grid(); EXPECT_EQ(5, energy.size()); EXPECT_SOFT_EQ(1.65e-6, energy.front()); EXPECT_SOFT_EQ(3.26e-6, energy.back()); - // Test the energy range and spectrum of emitted photons - GenericCalculator calc_energy(data_.energy_cdf[material_id_], data_.reals); + auto calc_energy = calc_cdf.make_inverse(); + auto const& cdf = calc_energy.grid(); + EXPECT_SOFT_EQ(0, cdf.front()); + EXPECT_SOFT_EQ(1, cdf.back()); EXPECT_SOFT_EQ(energy.front(), calc_energy(0)); EXPECT_SOFT_EQ(energy.back(), calc_energy(1)); diff --git a/test/corecel/CMakeLists.txt b/test/corecel/CMakeLists.txt index 463cf83296..28884786c9 100644 --- a/test/corecel/CMakeLists.txt +++ b/test/corecel/CMakeLists.txt @@ -71,6 +71,7 @@ celeritas_add_test(io/StringUtils.test.cc) celeritas_add_test(math/Algorithms.test.cc) celeritas_add_test(math/ArrayOperators.test.cc) celeritas_add_test(math/ArrayUtils.test.cc) +celeritas_add_test(math/CdfUtils.test.cc) celeritas_add_test(math/HashUtils.test.cc) celeritas_add_test(math/Integrator.test.cc) celeritas_add_device_test(math/NumericLimits) diff --git a/test/corecel/math/CdfUtils.test.cc b/test/corecel/math/CdfUtils.test.cc new file mode 100644 index 0000000000..999b3f1d35 --- /dev/null +++ b/test/corecel/math/CdfUtils.test.cc @@ -0,0 +1,87 @@ +//----------------------------------*-C++-*----------------------------------// +// Copyright 2024 UT-Battelle, LLC, and other Celeritas developers. +// See the top-level COPYRIGHT file for details. +// SPDX-License-Identifier: (Apache-2.0 OR MIT) +//---------------------------------------------------------------------------// +//! \file corecel/math/CdfUtils.test.cc +//---------------------------------------------------------------------------// +#include "corecel/math/CdfUtils.hh" + +#include "celeritas_test.hh" + +namespace celeritas +{ +namespace test +{ +//---------------------------------------------------------------------------// + +class CdfUtilsTest : public ::celeritas::test::Test +{ + protected: + void SetUp() override {} +}; + +TEST_F(CdfUtilsTest, segment_integrators) +{ + using Arr2 = Array; + EXPECT_SOFT_EQ( + 3.0, PostRectangleSegmentIntegrator{}(Arr2{-1, 0.5}, Arr2{5, 12345})); + EXPECT_SOFT_EQ(2.0, + TrapezoidSegmentIntegrator{}(Arr2{1, 0.5}, Arr2{3, 1.5})); +} + +TEST_F(CdfUtilsTest, integrate_segments) +{ + static double const x[] = {-1, 0, 1, 3, 6}; + static double const f[] = {1, 0, 2, 1, 0}; + std::vector dst(std::size(x)); + + { + SegmentIntegrator integrate_segments{PostRectangleSegmentIntegrator{}}; + integrate_segments(make_span(x), make_span(f), make_span(dst)); + + static double const expected_dst[] = {0, 1, 1, 5, 8}; + EXPECT_VEC_SOFT_EQ(expected_dst, dst); + + integrate_segments(make_span(x), make_span(f), make_span(dst), 1.0); + static double const expected_dst2[] = {1, 2, 2, 6, 9}; + EXPECT_VEC_SOFT_EQ(expected_dst2, dst); + } + + { + SegmentIntegrator integrate_segments{TrapezoidSegmentIntegrator{}}; + integrate_segments(make_span(x), make_span(f), make_span(dst)); + static double const expected_dst[] = {0, 0.5, 1.5, 4.5, 6}; + EXPECT_VEC_SOFT_EQ(expected_dst, dst); + } +} + +TEST_F(CdfUtilsTest, normalize_cdf) +{ + std::vector cdf = {1, 2, 4, 4, 8}; + + normalize_cdf(make_span(cdf)); + static double const expected_cdf[] = {0.125, 0.25, 0.5, 0.5, 1}; + EXPECT_VEC_SOFT_EQ(expected_cdf, cdf); + + if (CELERITAS_DEBUG) + { + // Empty + cdf.clear(); + EXPECT_THROW(normalize_cdf(make_span(cdf)), DebugError); + + // One and two zeros + cdf = {0.0}; + EXPECT_THROW(normalize_cdf(make_span(cdf)), DebugError); + cdf = {0.0, 0.0}; + EXPECT_THROW(normalize_cdf(make_span(cdf)), DebugError); + + // Nonmonotonic + cdf = {0, 1, 2, 1.5, 3}; + EXPECT_THROW(normalize_cdf(make_span(cdf)), DebugError); + } +} + +//---------------------------------------------------------------------------// +} // namespace test +} // namespace celeritas From e282d8846e4f639284fd4b5c7f3df39f956b4b72 Mon Sep 17 00:00:00 2001 From: Julien Esseiva Date: Mon, 9 Dec 2024 05:25:49 -0800 Subject: [PATCH 15/15] Fix clang-tidy weekly cron (#1541) --- .github/workflows/build-spack.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-spack.yml b/.github/workflows/build-spack.yml index 2b81fda78f..1035083d59 100644 --- a/.github/workflows/build-spack.yml +++ b/.github/workflows/build-spack.yml @@ -143,7 +143,7 @@ jobs: run: | if [ "${{github.event_name}}" == "schedule" ]; then echo "Full clang-tidy check on scheduled run." - ninja -k0 + ninja -Cbuild -k0 exit $? fi BASE=$(git merge-base origin/${BASE_REF} HEAD)