From 977ade79a1c66db0ff8542b3d231cccf6d7d1200 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Tue, 17 Feb 2026 09:50:38 -0600 Subject: [PATCH 01/38] Replace xtensor with internal Tensor/View classes (#3805) Co-authored-by: John Tramm --- .gitmodules | 6 - CMakeLists.txt | 19 +- cmake/OpenMCConfig.cmake.in | 2 - docs/source/quickinstall.rst | 2 +- include/openmc/bremsstrahlung.h | 12 +- include/openmc/distribution_energy.h | 8 +- include/openmc/eigenvalue.h | 4 +- include/openmc/hdf5_interface.h | 108 +- include/openmc/material.h | 4 +- include/openmc/mesh.h | 16 +- include/openmc/mgxs.h | 4 +- include/openmc/nuclide.h | 2 +- include/openmc/photon.h | 40 +- include/openmc/plot.h | 8 +- .../openmc/random_ray/flat_source_domain.h | 4 +- include/openmc/scattdata.h | 42 +- include/openmc/secondary_correlated.h | 8 +- include/openmc/secondary_kalbach.h | 12 +- include/openmc/secondary_thermal.h | 16 +- include/openmc/tallies/tally.h | 18 +- include/openmc/tensor.h | 1185 +++++++++++++++++ include/openmc/thermal.h | 2 +- include/openmc/urr.h | 8 +- include/openmc/volume_calc.h | 2 +- include/openmc/weight_windows.h | 18 +- include/openmc/wmp.h | 6 +- include/openmc/xml_interface.h | 8 +- include/openmc/xsdata.h | 24 +- src/bank.cpp | 1 + src/bremsstrahlung.cpp | 6 +- src/cmfd_solver.cpp | 34 +- src/cross_sections.cpp | 2 +- src/distribution_angle.cpp | 13 +- src/distribution_energy.cpp | 18 +- src/eigenvalue.cpp | 17 +- src/endf.cpp | 15 +- src/finalize.cpp | 4 +- src/hdf5_interface.cpp | 22 +- src/material.cpp | 43 +- src/mesh.cpp | 131 +- src/mgxs.cpp | 40 +- src/nuclide.cpp | 26 +- src/output.cpp | 2 +- src/photon.cpp | 81 +- src/physics.cpp | 7 +- src/physics_mg.cpp | 2 +- src/plot.cpp | 30 +- src/random_ray/flat_source_domain.cpp | 4 +- src/random_ray/random_ray.cpp | 2 + src/scattdata.cpp | 88 +- src/secondary_correlated.cpp | 34 +- src/secondary_kalbach.cpp | 23 +- src/secondary_thermal.cpp | 14 +- src/simulation.cpp | 4 +- src/source.cpp | 7 +- src/state_point.cpp | 55 +- src/tallies/filter_cell_instance.cpp | 3 +- src/tallies/filter_meshmaterial.cpp | 4 +- src/tallies/tally.cpp | 64 +- src/tallies/tally_scoring.cpp | 1 + src/tallies/trigger.cpp | 2 +- src/thermal.cpp | 11 +- src/track_output.cpp | 2 +- src/urr.cpp | 4 +- src/volume_calc.cpp | 14 +- src/weight_windows.cpp | 136 +- src/wmp.cpp | 14 +- src/xsdata.cpp | 459 ++++--- tests/cpp_unit_tests/CMakeLists.txt | 1 + tests/cpp_unit_tests/test_tensor.cpp | 987 ++++++++++++++ tests/regression_tests/cpp_driver/driver.cpp | 2 + vendor/xtensor | 1 - vendor/xtl | 1 - 73 files changed, 3111 insertions(+), 908 deletions(-) create mode 100644 include/openmc/tensor.h create mode 100644 tests/cpp_unit_tests/test_tensor.cpp delete mode 160000 vendor/xtensor delete mode 160000 vendor/xtl diff --git a/.gitmodules b/.gitmodules index f84d09bb1f..2bc1238943 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,12 +1,6 @@ [submodule "vendor/pugixml"] path = vendor/pugixml url = https://github.com/zeux/pugixml.git -[submodule "vendor/xtensor"] - path = vendor/xtensor - url = https://github.com/xtensor-stack/xtensor.git -[submodule "vendor/xtl"] - path = vendor/xtl - url = https://github.com/xtensor-stack/xtl.git [submodule "vendor/fmt"] path = vendor/fmt url = https://github.com/fmtlib/fmt.git diff --git a/CMakeLists.txt b/CMakeLists.txt index e29c0d81c6..5d6dd2a0ea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -266,23 +266,6 @@ else() endif() endif() -#=============================================================================== -# xtensor header-only library -#=============================================================================== - -if(OPENMC_FORCE_VENDORED_LIBS) - add_subdirectory(vendor/xtl) - set(xtl_DIR ${CMAKE_CURRENT_BINARY_DIR}/vendor/xtl) - add_subdirectory(vendor/xtensor) -else() - find_package_write_status(xtensor) - if (NOT xtensor_FOUND) - add_subdirectory(vendor/xtl) - set(xtl_DIR ${CMAKE_CURRENT_BINARY_DIR}/vendor/xtl) - add_subdirectory(vendor/xtensor) - endif() -endif() - #=============================================================================== # Catch2 library #=============================================================================== @@ -498,7 +481,7 @@ endif() # target_link_libraries treats any arguments starting with - but not -l as # linker flags. Thus, we can pass both linker flags and libraries together. target_link_libraries(libopenmc ${ldflags} ${HDF5_LIBRARIES} ${HDF5_HL_LIBRARIES} - xtensor fmt::fmt ${CMAKE_DL_LIBS}) + fmt::fmt ${CMAKE_DL_LIBS}) if(TARGET pugixml::pugixml) target_link_libraries(libopenmc pugixml::pugixml) diff --git a/cmake/OpenMCConfig.cmake.in b/cmake/OpenMCConfig.cmake.in index 837a39c783..cc837bba71 100644 --- a/cmake/OpenMCConfig.cmake.in +++ b/cmake/OpenMCConfig.cmake.in @@ -5,8 +5,6 @@ get_filename_component(_OPENMC_PREFIX "${OpenMC_CMAKE_DIR}/../../.." ABSOLUTE) find_package(fmt CONFIG REQUIRED HINTS ${_OPENMC_PREFIX}) find_package(pugixml CONFIG REQUIRED HINTS ${_OPENMC_PREFIX}) -find_package(xtl CONFIG REQUIRED HINTS ${_OPENMC_PREFIX}) -find_package(xtensor CONFIG REQUIRED HINTS ${_OPENMC_PREFIX}) if(@OPENMC_USE_DAGMC@) find_package(DAGMC REQUIRED HINTS @DAGMC_DIR@) endif() diff --git a/docs/source/quickinstall.rst b/docs/source/quickinstall.rst index 0f887940ea..6cb774b5b0 100644 --- a/docs/source/quickinstall.rst +++ b/docs/source/quickinstall.rst @@ -119,7 +119,7 @@ packages should be installed, for example in Homebrew via: .. code-block:: sh - brew install llvm cmake xtensor hdf5 python libomp libpng + brew install llvm cmake hdf5 python libomp libpng The compiler provided by the above LLVM package should be used in place of the one provisioned by XCode, which does not support the multithreading library used diff --git a/include/openmc/bremsstrahlung.h b/include/openmc/bremsstrahlung.h index 2f7e41bf08..d3b5868317 100644 --- a/include/openmc/bremsstrahlung.h +++ b/include/openmc/bremsstrahlung.h @@ -3,7 +3,7 @@ #include "openmc/particle.h" -#include "xtensor/xtensor.hpp" +#include "openmc/tensor.h" namespace openmc { @@ -14,9 +14,9 @@ namespace openmc { class BremsstrahlungData { public: // Data - xt::xtensor pdf; //!< Bremsstrahlung energy PDF - xt::xtensor cdf; //!< Bremsstrahlung energy CDF - xt::xtensor yield; //!< Photon yield + tensor::Tensor pdf; //!< Bremsstrahlung energy PDF + tensor::Tensor cdf; //!< Bremsstrahlung energy CDF + tensor::Tensor yield; //!< Photon yield }; class Bremsstrahlung { @@ -32,9 +32,9 @@ public: namespace data { -extern xt::xtensor +extern tensor::Tensor ttb_e_grid; //! energy T of incident electron in [eV] -extern xt::xtensor +extern tensor::Tensor ttb_k_grid; //! reduced energy W/T of emitted photon } // namespace data diff --git a/include/openmc/distribution_energy.h b/include/openmc/distribution_energy.h index 9b08ed039d..efacb91319 100644 --- a/include/openmc/distribution_energy.h +++ b/include/openmc/distribution_energy.h @@ -5,7 +5,7 @@ #define OPENMC_DISTRIBUTION_ENERGY_H #include "hdf5.h" -#include "xtensor/xtensor.hpp" +#include "openmc/tensor.h" #include "openmc/constants.h" #include "openmc/endf.h" @@ -86,9 +86,9 @@ private: struct CTTable { Interpolation interpolation; //!< Interpolation law int n_discrete; //!< Number of of discrete energies - xt::xtensor e_out; //!< Outgoing energies in [eV] - xt::xtensor p; //!< Probability density - xt::xtensor c; //!< Cumulative distribution + tensor::Tensor e_out; //!< Outgoing energies in [eV] + tensor::Tensor p; //!< Probability density + tensor::Tensor c; //!< Cumulative distribution }; int n_region_; //!< Number of inteprolation regions diff --git a/include/openmc/eigenvalue.h b/include/openmc/eigenvalue.h index b456fee21e..4387476760 100644 --- a/include/openmc/eigenvalue.h +++ b/include/openmc/eigenvalue.h @@ -6,7 +6,7 @@ #include // for int64_t -#include "xtensor/xtensor.hpp" +#include "openmc/tensor.h" #include #include "openmc/array.h" @@ -24,7 +24,7 @@ namespace simulation { extern double keff_generation; //!< Single-generation k on each processor extern array k_sum; //!< Used to reduce sum and sum_sq extern vector entropy; //!< Shannon entropy at each generation -extern xt::xtensor source_frac; //!< Source fraction for UFS +extern tensor::Tensor source_frac; //!< Source fraction for UFS } // namespace simulation diff --git a/include/openmc/hdf5_interface.h b/include/openmc/hdf5_interface.h index 28b0d2b113..93e4bfc820 100644 --- a/include/openmc/hdf5_interface.h +++ b/include/openmc/hdf5_interface.h @@ -11,8 +11,7 @@ #include "hdf5.h" #include "hdf5_hl.h" -#include "xtensor/xadapt.hpp" -#include "xtensor/xarray.hpp" +#include "openmc/tensor.h" #include "openmc/array.h" #include "openmc/error.h" @@ -166,24 +165,19 @@ void read_attribute(hid_t obj_id, const char* name, vector& vec) read_attr(obj_id, name, H5TypeMap::type_id, vec.data()); } -// Generic array version +// Tensor version template -void read_attribute(hid_t obj_id, const char* name, xt::xarray& arr) +void read_attribute(hid_t obj_id, const char* name, tensor::Tensor& tensor) { - // Get shape of attribute array + // Get shape of attribute auto shape = attribute_shape(obj_id, name); - // Allocate new array to read data into - std::size_t size = 1; - for (const auto x : shape) - size *= x; - vector buffer(size); + // Resize tensor and read data directly + vector tshape(shape.begin(), shape.end()); + tensor.resize(tshape); // Read data from attribute - read_attr(obj_id, name, H5TypeMap::type_id, buffer.data()); - - // Adapt array into xarray - arr = xt::adapt(buffer, shape); + read_attr(obj_id, name, H5TypeMap::type_id, tensor.data()); } // overload for std::string @@ -290,63 +284,34 @@ void read_dataset( } template -void read_dataset(hid_t dset, xt::xarray& arr, bool indep = false) +void read_dataset(hid_t dset, tensor::Tensor& tensor, bool indep = false) { // Get shape of dataset vector shape = object_shape(dset); - // Allocate space in the array to read data into - std::size_t size = 1; - for (const auto x : shape) - size *= x; - arr.resize(shape); + // Resize tensor and read data directly + vector tshape(shape.begin(), shape.end()); + tensor.resize(tshape); - // Read data from attribute + // Read data from dataset read_dataset_lowlevel( - dset, nullptr, H5TypeMap::type_id, H5S_ALL, indep, arr.data()); + dset, nullptr, H5TypeMap::type_id, H5S_ALL, indep, tensor.data()); } template<> void read_dataset( - hid_t dset, xt::xarray>& arr, bool indep); + hid_t dset, tensor::Tensor>& tensor, bool indep); template void read_dataset( - hid_t obj_id, const char* name, xt::xarray& arr, bool indep = false) + hid_t obj_id, const char* name, tensor::Tensor& tensor, bool indep = false) { - // Open dataset and read array + // Open dataset and read tensor hid_t dset = open_dataset(obj_id, name); - read_dataset(dset, arr, indep); + read_dataset(dset, tensor, indep); close_dataset(dset); } -template -void read_dataset( - hid_t obj_id, const char* name, xt::xtensor& arr, bool indep = false) -{ - // Open dataset and read array - hid_t dset = open_dataset(obj_id, name); - - // Get shape of dataset - vector hsize_t_shape = object_shape(dset); - close_dataset(dset); - - // cast from hsize_t to size_t - vector shape(hsize_t_shape.size()); - for (int i = 0; i < shape.size(); i++) { - shape[i] = static_cast(hsize_t_shape[i]); - } - - // Allocate new xarray to read data into - xt::xarray xarr(shape); - - // Read data from the dataset - read_dataset(obj_id, name, xarr); - - // Copy into xtensor - arr = xarr; -} - // overload for Position inline void read_dataset( hid_t obj_id, const char* name, Position& r, bool indep = false) @@ -358,31 +323,22 @@ inline void read_dataset( r.z = x[2]; } -template +template inline void read_dataset_as_shape( - hid_t obj_id, const char* name, xt::xtensor& arr, bool indep = false) + hid_t obj_id, const char* name, tensor::Tensor& tensor, bool indep = false) { hid_t dset = open_dataset(obj_id, name); - // Allocate new array to read data into - std::size_t size = 1; - for (const auto x : arr.shape()) - size *= x; - vector buffer(size); - - // Read data from attribute + // Read data directly into pre-shaped tensor read_dataset_lowlevel( - dset, nullptr, H5TypeMap::type_id, H5S_ALL, indep, buffer.data()); - - // Adapt into xarray - arr = xt::adapt(buffer, arr.shape()); + dset, nullptr, H5TypeMap::type_id, H5S_ALL, indep, tensor.data()); close_dataset(dset); } -template -inline void read_nd_vector(hid_t obj_id, const char* name, - xt::xtensor& result, bool must_have = false) +template +inline void read_nd_tensor(hid_t obj_id, const char* name, + tensor::Tensor& result, bool must_have = false) { if (object_exists(obj_id, name)) { read_dataset_as_shape(obj_id, name, result, true); @@ -496,12 +452,16 @@ inline void write_dataset( false, buffer.data()); } -// Template for xarray, xtensor, etc. -template -inline void write_dataset( - hid_t obj_id, const char* name, const xt::xcontainer& arr) +// Template for Tensor and StaticTensor2D. A SFINAE guard is used here to +// prevent this template from matching vector/string types that have their own +// overloads above. A generic Container parameter avoids duplicating the body +// for both Tensor and StaticTensor2D. +template>::value>> +inline void write_dataset(hid_t obj_id, const char* name, const Container& arr) { - using T = typename D::value_type; + using T = typename std::decay_t::value_type; auto s = arr.shape(); vector dims {s.cbegin(), s.cend()}; write_dataset_lowlevel(obj_id, dims.size(), dims.data(), name, diff --git a/include/openmc/material.h b/include/openmc/material.h index c10f25551e..3967c36878 100644 --- a/include/openmc/material.h +++ b/include/openmc/material.h @@ -5,8 +5,8 @@ #include #include "openmc/span.h" +#include "openmc/tensor.h" #include "pugixml.hpp" -#include "xtensor/xtensor.hpp" #include #include "openmc/bremsstrahlung.h" @@ -189,7 +189,7 @@ public: vector nuclide_; //!< Indices in nuclides vector vector element_; //!< Indices in elements vector NCrystalMat ncrystal_mat_; //!< NCrystal material object - xt::xtensor atom_density_; //!< Nuclide atom density in [atom/b-cm] + tensor::Tensor atom_density_; //!< Nuclide atom density in [atom/b-cm] double density_; //!< Total atom density in [atom/b-cm] double density_gpcc_; //!< Total atom density in [g/cm^3] double charge_density_; //!< Total charge density in [e/b-cm] diff --git a/include/openmc/mesh.h b/include/openmc/mesh.h index 5dc327fe35..0d8189caa1 100644 --- a/include/openmc/mesh.h +++ b/include/openmc/mesh.h @@ -8,8 +8,8 @@ #include #include "hdf5.h" +#include "openmc/tensor.h" #include "pugixml.hpp" -#include "xtensor/xtensor.hpp" #include "openmc/bounding_box.h" #include "openmc/error.h" @@ -284,8 +284,8 @@ public: virtual Position upper_right() const = 0; // Data members - xt::xtensor lower_left_; //!< Lower-left coordinates of mesh - xt::xtensor upper_right_; //!< Upper-right coordinates of mesh + tensor::Tensor lower_left_; //!< Lower-left coordinates of mesh + tensor::Tensor upper_right_; //!< Upper-right coordinates of mesh int id_ {-1}; //!< Mesh ID std::string name_; //!< User-specified name int n_dimension_ {-1}; //!< Number of dimensions @@ -348,7 +348,7 @@ public: //! \param[in] Pointer to bank sites //! \param[in] Number of bank sites //! \param[out] Whether any bank sites are outside the mesh - xt::xtensor count_sites( + tensor::Tensor count_sites( const SourceSite* bank, int64_t length, bool* outside) const; //! Get bin given mesh indices @@ -419,8 +419,8 @@ public: //! Get a label for the mesh bin std::string bin_label(int bin) const override; - //! Get shape as xt::xtensor - xt::xtensor get_x_shape() const; + //! Get mesh dimensions as a tensor + tensor::Tensor get_shape_tensor() const; double volume(int bin) const override { @@ -515,7 +515,7 @@ public: //! \param[in] bank Array of bank sites //! \param[out] Whether any bank sites are outside the mesh //! \return Array indicating number of sites in each mesh/energy bin - xt::xtensor count_sites( + tensor::Tensor count_sites( const SourceSite* bank, int64_t length, bool* outside) const; //! Return the volume for a given mesh index @@ -526,7 +526,7 @@ public: // Data members double volume_frac_; //!< Volume fraction of each mesh element double element_volume_; //!< Volume of each mesh element - xt::xtensor width_; //!< Width of each mesh element + tensor::Tensor width_; //!< Width of each mesh element }; class RectilinearMesh : public StructuredMesh { diff --git a/include/openmc/mgxs.h b/include/openmc/mgxs.h index 43505f432d..5d8ff58c23 100644 --- a/include/openmc/mgxs.h +++ b/include/openmc/mgxs.h @@ -6,7 +6,7 @@ #include -#include "xtensor/xtensor.hpp" +#include "openmc/tensor.h" #include "openmc/constants.h" #include "openmc/hdf5_interface.h" @@ -22,7 +22,7 @@ namespace openmc { class Mgxs { private: - xt::xtensor kTs; // temperature in eV (k * T) + tensor::Tensor kTs; // temperature in eV (k * T) AngleDistributionType scatter_format; // flag for if this is legendre, histogram, or tabular int num_groups; // number of energy groups diff --git a/include/openmc/nuclide.h b/include/openmc/nuclide.h index 58d8339396..7a8b2acadd 100644 --- a/include/openmc/nuclide.h +++ b/include/openmc/nuclide.h @@ -96,7 +96,7 @@ public: // Temperature dependent cross section data vector kTs_; //!< temperatures in eV (k*T) vector grid_; //!< Energy grid at each temperature - vector> xs_; //!< Cross sections at each temperature + vector> xs_; //!< Cross sections at each temperature // Multipole data unique_ptr multipole_; diff --git a/include/openmc/photon.h b/include/openmc/photon.h index f6f28a4df1..93c6dba53f 100644 --- a/include/openmc/photon.h +++ b/include/openmc/photon.h @@ -6,7 +6,7 @@ #include "openmc/particle.h" #include "openmc/vector.h" -#include "xtensor/xtensor.hpp" +#include "openmc/tensor.h" #include #include @@ -62,14 +62,14 @@ public: int64_t index_; //!< Index in global elements vector // Microscopic cross sections - xt::xtensor energy_; - xt::xtensor coherent_; - xt::xtensor incoherent_; - xt::xtensor photoelectric_total_; - xt::xtensor pair_production_total_; - xt::xtensor pair_production_electron_; - xt::xtensor pair_production_nuclear_; - xt::xtensor heating_; + tensor::Tensor energy_; + tensor::Tensor coherent_; + tensor::Tensor incoherent_; + tensor::Tensor photoelectric_total_; + tensor::Tensor pair_production_total_; + tensor::Tensor pair_production_electron_; + tensor::Tensor pair_production_nuclear_; + tensor::Tensor heating_; // Form factors Tabulated1D incoherent_form_factor_; @@ -81,27 +81,27 @@ public: // stored separately to improve memory access pattern when calculating the // total cross section vector shells_; - xt::xtensor cross_sections_; + tensor::Tensor cross_sections_; // Compton profile data - xt::xtensor profile_pdf_; - xt::xtensor profile_cdf_; - xt::xtensor binding_energy_; - xt::xtensor electron_pdf_; + tensor::Tensor profile_pdf_; + tensor::Tensor profile_cdf_; + tensor::Tensor binding_energy_; + tensor::Tensor electron_pdf_; // Map subshells from Compton profile data obtained from Biggs et al, // "Hartree-Fock Compton profiles for the elements" to ENDF/B atomic // relaxation data - xt::xtensor subshell_map_; + tensor::Tensor subshell_map_; // Stopping power data double I_; // mean excitation energy - xt::xtensor n_electrons_; - xt::xtensor ionization_energy_; - xt::xtensor stopping_power_radiative_; + tensor::Tensor n_electrons_; + tensor::Tensor ionization_energy_; + tensor::Tensor stopping_power_radiative_; // Bremsstrahlung scaled DCS - xt::xtensor dcs_; + tensor::Tensor dcs_; // Whether atomic relaxation data is present bool has_atomic_relaxation_ {false}; @@ -137,7 +137,7 @@ void free_memory_photon(); namespace data { -extern xt::xtensor +extern tensor::Tensor compton_profile_pz; //! Compton profile momentum grid //! Photon interaction data for each element diff --git a/include/openmc/plot.h b/include/openmc/plot.h index 3813101c65..cd6be43b72 100644 --- a/include/openmc/plot.h +++ b/include/openmc/plot.h @@ -6,8 +6,8 @@ #include #include +#include "openmc/tensor.h" #include "pugixml.hpp" -#include "xtensor/xarray.hpp" #include "hdf5.h" #include "openmc/cell.h" @@ -90,7 +90,7 @@ const RGBColor BLACK {0, 0, 0}; * visualized. */ -typedef xt::xtensor ImageData; +typedef tensor::Tensor ImageData; class PlottableInterface { public: PlottableInterface() = default; @@ -154,7 +154,7 @@ struct IdData { void set_overlap(size_t y, size_t x); // Members - xt::xtensor data_; //!< 2D array of cell & material ids + tensor::Tensor data_; //!< 2D array of cell & material ids }; struct PropertyData { @@ -166,7 +166,7 @@ struct PropertyData { void set_overlap(size_t y, size_t x); // Members - xt::xtensor data_; //!< 2D array of temperature & density data + tensor::Tensor data_; //!< 2D array of temperature & density data }; //=============================================================================== diff --git a/include/openmc/random_ray/flat_source_domain.h b/include/openmc/random_ray/flat_source_domain.h index 693093683a..c40982712e 100644 --- a/include/openmc/random_ray/flat_source_domain.h +++ b/include/openmc/random_ray/flat_source_domain.h @@ -178,10 +178,10 @@ protected: // Volumes for each tally and bin/score combination. This intermediate data // structure is used when tallying quantities that must be normalized by // volume (i.e., flux). The vector is index by tally index, while the inner 2D - // xtensor is indexed by bin index and score index in a similar manner to the + // tensor is indexed by bin index and score index in a similar manner to the // results tensor in the Tally class, though without the third dimension, as // SUM and SUM_SQ do not need to be tracked. - vector> tally_volumes_; + vector> tally_volumes_; }; // class FlatSourceDomain diff --git a/include/openmc/scattdata.h b/include/openmc/scattdata.h index a75ef09d97..bea881402a 100644 --- a/include/openmc/scattdata.h +++ b/include/openmc/scattdata.h @@ -4,7 +4,7 @@ #ifndef OPENMC_SCATTDATA_H #define OPENMC_SCATTDATA_H -#include "xtensor/xtensor.hpp" +#include "openmc/tensor.h" #include "openmc/constants.h" #include "openmc/vector.h" @@ -26,23 +26,23 @@ public: protected: //! \brief Initializes the attributes of the base class. - void base_init(int order, const xt::xtensor& in_gmin, - const xt::xtensor& in_gmax, const double_2dvec& in_energy, + void base_init(int order, const tensor::Tensor& in_gmin, + const tensor::Tensor& in_gmax, const double_2dvec& in_energy, const double_2dvec& in_mult); //! \brief Combines microscopic ScattDatas into a macroscopic one. void base_combine(size_t max_order, size_t order_dim, const vector& those_scatts, const vector& scalars, - xt::xtensor& in_gmin, xt::xtensor& in_gmax, + tensor::Tensor& in_gmin, tensor::Tensor& in_gmax, double_2dvec& sparse_mult, double_3dvec& sparse_scatter); public: double_2dvec energy; // Normalized p0 matrix for sampling Eout double_2dvec mult; // nu-scatter multiplication (nu-scatt/scatt) double_3dvec dist; // Angular distribution - xt::xtensor gmin; // minimum outgoing group - xt::xtensor gmax; // maximum outgoing group - xt::xtensor scattxs; // Isotropic Sigma_{s,g_{in}} + tensor::Tensor gmin; // minimum outgoing group + tensor::Tensor gmax; // maximum outgoing group + tensor::Tensor scattxs; // Isotropic Sigma_{s,g_{in}} //! \brief Calculates the value of normalized f(mu). //! @@ -72,8 +72,8 @@ public: //! @param in_gmax List of maximum outgoing groups for every incoming group //! @param in_mult Input sparse multiplicity matrix //! @param coeffs Input sparse scattering matrix - virtual void init(const xt::xtensor& in_gmin, - const xt::xtensor& in_gmax, const double_2dvec& in_mult, + virtual void init(const tensor::Tensor& in_gmin, + const tensor::Tensor& in_gmax, const double_2dvec& in_mult, const double_3dvec& coeffs) = 0; //! \brief Combines the microscopic data. @@ -96,7 +96,7 @@ public: //! @param max_order If Legendre this is the maximum value of "n" in "Pn" //! requested; ignored otherwise. //! @return The dense scattering matrix. - virtual xt::xtensor get_matrix(size_t max_order) = 0; + virtual tensor::Tensor get_matrix(size_t max_order) = 0; //! \brief Samples the outgoing energy from the ScattData info. //! @@ -135,8 +135,8 @@ protected: ScattDataLegendre& leg, ScattDataTabular& tab); public: - void init(const xt::xtensor& in_gmin, - const xt::xtensor& in_gmax, const double_2dvec& in_mult, + void init(const tensor::Tensor& in_gmin, + const tensor::Tensor& in_gmax, const double_2dvec& in_mult, const double_3dvec& coeffs) override; void combine(const vector& those_scatts, @@ -153,7 +153,7 @@ public: size_t get_order() override { return dist[0][0].size() - 1; }; - xt::xtensor get_matrix(size_t max_order) override; + tensor::Tensor get_matrix(size_t max_order) override; }; //============================================================================== @@ -164,13 +164,13 @@ public: class ScattDataHistogram : public ScattData { protected: - xt::xtensor mu; // Angle distribution mu bin boundaries + tensor::Tensor mu; // Angle distribution mu bin boundaries double dmu; // Quick storage of the mu spacing double_3dvec fmu; // The angular distribution histogram public: - void init(const xt::xtensor& in_gmin, - const xt::xtensor& in_gmax, const double_2dvec& in_mult, + void init(const tensor::Tensor& in_gmin, + const tensor::Tensor& in_gmax, const double_2dvec& in_mult, const double_3dvec& coeffs) override; void combine(const vector& those_scatts, @@ -183,7 +183,7 @@ public: size_t get_order() override { return dist[0][0].size(); }; - xt::xtensor get_matrix(size_t max_order) override; + tensor::Tensor get_matrix(size_t max_order) override; }; //============================================================================== @@ -194,7 +194,7 @@ public: class ScattDataTabular : public ScattData { protected: - xt::xtensor mu; // Angle distribution mu grid points + tensor::Tensor mu; // Angle distribution mu grid points double dmu; // Quick storage of the mu spacing double_3dvec fmu; // The angular distribution function @@ -204,8 +204,8 @@ protected: ScattDataLegendre& leg, ScattDataTabular& tab); public: - void init(const xt::xtensor& in_gmin, - const xt::xtensor& in_gmax, const double_2dvec& in_mult, + void init(const tensor::Tensor& in_gmin, + const tensor::Tensor& in_gmax, const double_2dvec& in_mult, const double_3dvec& coeffs) override; void combine(const vector& those_scatts, @@ -218,7 +218,7 @@ public: size_t get_order() override { return dist[0][0].size(); }; - xt::xtensor get_matrix(size_t max_order) override; + tensor::Tensor get_matrix(size_t max_order) override; }; //============================================================================== diff --git a/include/openmc/secondary_correlated.h b/include/openmc/secondary_correlated.h index 6905c38e36..b4b7f8480f 100644 --- a/include/openmc/secondary_correlated.h +++ b/include/openmc/secondary_correlated.h @@ -5,7 +5,7 @@ #define OPENMC_SECONDARY_CORRELATED_H #include "hdf5.h" -#include "xtensor/xtensor.hpp" +#include "openmc/tensor.h" #include "openmc/angle_energy.h" #include "openmc/distribution.h" @@ -25,9 +25,9 @@ public: struct CorrTable { int n_discrete; //!< Number of discrete lines Interpolation interpolation; //!< Interpolation law - xt::xtensor e_out; //!< Outgoing energies [eV] - xt::xtensor p; //!< Probability density - xt::xtensor c; //!< Cumulative distribution + tensor::Tensor e_out; //!< Outgoing energies [eV] + tensor::Tensor p; //!< Probability density + tensor::Tensor c; //!< Cumulative distribution vector> angle; //!< Angle distribution }; diff --git a/include/openmc/secondary_kalbach.h b/include/openmc/secondary_kalbach.h index 83806d3524..c9c5849bc7 100644 --- a/include/openmc/secondary_kalbach.h +++ b/include/openmc/secondary_kalbach.h @@ -5,7 +5,7 @@ #define OPENMC_SECONDARY_KALBACH_H #include "hdf5.h" -#include "xtensor/xtensor.hpp" +#include "openmc/tensor.h" #include "openmc/angle_energy.h" #include "openmc/constants.h" @@ -37,11 +37,11 @@ private: struct KMTable { int n_discrete; //!< Number of discrete lines Interpolation interpolation; //!< Interpolation law - xt::xtensor e_out; //!< Outgoing energies [eV] - xt::xtensor p; //!< Probability density - xt::xtensor c; //!< Cumulative distribution - xt::xtensor r; //!< Pre-compound fraction - xt::xtensor a; //!< Parameterized function + tensor::Tensor e_out; //!< Outgoing energies [eV] + tensor::Tensor p; //!< Probability density + tensor::Tensor c; //!< Cumulative distribution + tensor::Tensor r; //!< Pre-compound fraction + tensor::Tensor a; //!< Parameterized function }; int n_region_; //!< Number of interpolation regions diff --git a/include/openmc/secondary_thermal.h b/include/openmc/secondary_thermal.h index 5b18902afb..4f33c0e763 100644 --- a/include/openmc/secondary_thermal.h +++ b/include/openmc/secondary_thermal.h @@ -9,7 +9,7 @@ #include "openmc/secondary_correlated.h" #include "openmc/vector.h" -#include "xtensor/xtensor.hpp" +#include "openmc/tensor.h" #include namespace openmc { @@ -83,7 +83,7 @@ public: private: const vector& energy_; //!< Energies at which cosines are tabulated - xt::xtensor mu_out_; //!< Cosines for each incident energy + tensor::Tensor mu_out_; //!< Cosines for each incident energy }; //============================================================================== @@ -109,9 +109,9 @@ public: private: const vector& energy_; //!< Incident energies - xt::xtensor + tensor::Tensor energy_out_; //!< Outgoing energies for each incident energy - xt::xtensor + tensor::Tensor mu_out_; //!< Outgoing cosines for each incident/outgoing energy bool skewed_; //!< Whether outgoing energy distribution is skewed }; @@ -139,10 +139,10 @@ private: //! Secondary energy/angle distribution struct DistEnergySab { std::size_t n_e_out; //!< Number of outgoing energies - xt::xtensor e_out; //!< Outgoing energies - xt::xtensor e_out_pdf; //!< Probability density function - xt::xtensor e_out_cdf; //!< Cumulative distribution function - xt::xtensor mu; //!< Equiprobable angles at each outgoing energy + tensor::Tensor e_out; //!< Outgoing energies + tensor::Tensor e_out_pdf; //!< Probability density function + tensor::Tensor e_out_cdf; //!< Cumulative distribution function + tensor::Tensor mu; //!< Equiprobable angles at each outgoing energy }; vector energy_; //!< Incident energies diff --git a/include/openmc/tallies/tally.h b/include/openmc/tallies/tally.h index 374daff92a..6161f823be 100644 --- a/include/openmc/tallies/tally.h +++ b/include/openmc/tallies/tally.h @@ -8,9 +8,8 @@ #include "openmc/tallies/trigger.h" #include "openmc/vector.h" +#include "openmc/tensor.h" #include "pugixml.hpp" -#include "xtensor/xfixed.hpp" -#include "xtensor/xtensor.hpp" #include #include @@ -55,7 +54,7 @@ public: void set_nuclides(const vector& nuclides); - const xt::xtensor& results() const { return results_; } + const tensor::Tensor& results() const { return results_; } //! returns vector of indices corresponding to the tally this is called on const vector& filters() const { return filters_; } @@ -125,7 +124,7 @@ public: int score_index(const std::string& score) const; //! Tally results reshaped according to filter sizes - xt::xarray get_reshaped_data() const; + tensor::Tensor get_reshaped_data() const; //! A string representing the i-th score on this tally std::string score_name(int score_idx) const; @@ -160,7 +159,7 @@ public: //! combination of filters (e.g. specific cell, specific energy group, etc.) //! and the second dimension of the array is for scores (e.g. flux, total //! reaction rate, fission reaction rate, etc.) - xt::xtensor results_; + tensor::Tensor results_; //! True if this tally should be written to statepoint files bool writable_ {true}; @@ -220,8 +219,7 @@ extern vector time_grid; namespace simulation { //! Global tallies (such as k-effective estimators) -extern xt::xtensor_fixed> - global_tallies; +extern tensor::StaticTensor2D global_tallies; //! Number of realizations for global tallies extern "C" int32_t n_realizations; @@ -257,12 +255,6 @@ double distance_to_time_boundary(double time, double speed); //! Determine which tallies should be active void setup_active_tallies(); -// Alias for the type returned by xt::adapt(...). N is the dimension of the -// multidimensional array -template -using adaptor_type = - xt::xtensor_adaptor, N>; - #ifdef OPENMC_MPI //! Collect all tally results onto master process void reduce_tally_results(); diff --git a/include/openmc/tensor.h b/include/openmc/tensor.h new file mode 100644 index 0000000000..9d428aaebd --- /dev/null +++ b/include/openmc/tensor.h @@ -0,0 +1,1185 @@ +//! \file tensor.h +//! \brief Multi-dimensional tensor types for OpenMC. +//! +//! Tensor is the primary type: a dynamic-rank owning container that stores +//! elements contiguously in row-major order. View is a lightweight +//! non-owning reference into a Tensor's storage, returned by the slice() +//! method and flat(). StaticTensor2D is a small stack-allocated 2D +//! array used only for simulation::global_tallies. +//! +//! Slicing follows numpy conventions: each axis takes an index (rank-reducing), +//! All (keep entire axis), or Range (keep sub-range). For example, +//! arr.slice(0, all, range(2, 5)) is equivalent to numpy's arr[0, :, 2:5]. +//! +//! View is declared before Tensor because Tensor's methods return View objects. + +#ifndef OPENMC_TENSOR_H +#define OPENMC_TENSOR_H + +#include "openmc/vector.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace openmc { +namespace tensor { + +//============================================================================== +// Forward declarations +//============================================================================== + +template +class Tensor; + +template +class StaticTensor2D; + +//============================================================================== +// Storage type mapping +// +// std::vector is a bit-packed specialization that returns proxy objects +// instead of real references, which breaks generic code. storage_type_map +// redirects bool to unsigned char so that Tensor stores one byte per +// element with normal reference semantics. +//============================================================================== + +template +struct storage_type_map { + using type = T; +}; +template<> +struct storage_type_map { + using type = unsigned char; +}; +template +using storage_type = typename storage_type_map::type; + +//============================================================================== +// Slice argument types +// +// Used with the variadic slice() method on Tensor, View, and StaticTensor2D. +// Each argument corresponds to one axis: a plain integer fixes that axis at +// a single index (rank-reducing), All keeps the entire axis, and Range keeps +// a sub-range. +//============================================================================== + +//! Keep an entire axis (equivalent to numpy's ':' or xtensor's xt::all()) +struct All {}; +constexpr All all {}; + +//! Sub-range along an axis [start, end) +struct Range { + size_t start; + size_t end; // SIZE_MAX means "to end of axis" +}; + +//! Create a Range [start, end) +inline Range range(size_t start, size_t end) +{ + return {start, end}; +} + +//! Create a Range [0, end) +inline Range range(size_t end) +{ + return {0, end}; +} + +namespace detail { + +//! Internal: normalized representation of a per-axis slice argument +struct SliceArg { + enum Kind { INDEX, ALL, RANGE } kind; + size_t start; + size_t end; +}; + +inline SliceArg to_slice_arg(All) +{ + return {SliceArg::ALL, 0, 0}; +} +inline SliceArg to_slice_arg(Range r) +{ + return {SliceArg::RANGE, r.start, r.end}; +} + +template +inline + typename std::enable_if::value || std::is_enum::value, + SliceArg>::type + to_slice_arg(I i) +{ + return {SliceArg::INDEX, static_cast(i), 0}; +} + +//! Result of a slice computation: pointer offset + new shape/strides +struct SliceResult { + size_t ptr_offset; + vector shape; + vector strides; +}; + +//! Compute the result of applying slice arguments to shape/strides +template +SliceResult compute_slice(const vector& shape, + const vector& strides, First first, Rest... rest) +{ + const size_t n = 1 + sizeof...(Rest); + SliceArg args[1 + sizeof...(Rest)] = { + to_slice_arg(first), to_slice_arg(rest)...}; + + size_t offset = 0; + vector new_shape; + vector new_strides; + + for (size_t a = 0; a < n; ++a) { + switch (args[a].kind) { + case SliceArg::INDEX: + offset += args[a].start * strides[a]; + break; + case SliceArg::ALL: + new_shape.push_back(shape[a]); + new_strides.push_back(strides[a]); + break; + case SliceArg::RANGE: { + offset += args[a].start * strides[a]; + size_t end = (args[a].end == SIZE_MAX) ? shape[a] : args[a].end; + new_shape.push_back(end - args[a].start); + new_strides.push_back(strides[a]); + break; + } + } + } + + // Trailing axes not covered by arguments are implicitly All. + // This matches numpy: a[i] on a 2D array returns a 1D row. + for (size_t a = n; a < shape.size(); ++a) { + new_shape.push_back(shape[a]); + new_strides.push_back(strides[a]); + } + + return {offset, std::move(new_shape), std::move(new_strides)}; +} + +} // namespace detail + +//============================================================================== +// View: a non-owning N-dimensional view into a tensor's storage. +// +// Holds a base pointer, shape, and strides (in elements). Supports arbitrary +// rank and multi-axis slicing via the variadic slice() method. +//============================================================================== + +template +class View { +public: + //-------------------------------------------------------------------------- + // Constructors + + View(T* data, vector shape, vector strides) + : data_(data), shape_(std::move(shape)), strides_(std::move(strides)) + {} + + // Explicitly default copy/move constructors (declaring copy assignment + // below would otherwise suppress the implicit move constructor). + View(const View&) = default; + View(View&&) = default; + + //-------------------------------------------------------------------------- + // Indexing + + //! Multi-index element access (1D, 2D, 3D, ...) + template + T& operator()(Indices... indices) + { + const size_t idx[] = {static_cast(indices)...}; + size_t off = 0; + for (size_t d = 0; d < sizeof...(Indices); ++d) + off += idx[d] * strides_[d]; + return data_[off]; + } + + template + const T& operator()(Indices... indices) const + { + const size_t idx[] = {static_cast(indices)...}; + size_t off = 0; + for (size_t d = 0; d < sizeof...(Indices); ++d) + off += idx[d] * strides_[d]; + return data_[off]; + } + + //! Flat logical index (row-major order) + T& operator[](size_t i) { return data_[flat_to_offset(i)]; } + const T& operator[](size_t i) const { return data_[flat_to_offset(i)]; } + + //-------------------------------------------------------------------------- + // Accessors + + size_t size() const + { + size_t s = 1; + for (auto d : shape_) + s *= d; + return s; + } + size_t ndim() const { return shape_.size(); } + size_t shape(size_t axis) const { return shape_[axis]; } + const vector& shape_vec() const { return shape_; } + T* data() { return data_; } + const T* data() const { return data_; } + + //-------------------------------------------------------------------------- + // View accessors + + //! Multi-axis slice. Each argument corresponds to one axis and is either: + //! - an integer (fixes that axis, rank-reducing) + //! - All (keeps entire axis) + //! - Range (keeps sub-range along that axis) + //! Example: v.slice(0, all, range(2, 5)) == numpy v[0, :, 2:5] + template + View slice(First first, Rest... rest) + { + auto r = detail::compute_slice(shape_, strides_, first, rest...); + return {data_ + r.ptr_offset, std::move(r.shape), std::move(r.strides)}; + } + + template + View slice(First first, Rest... rest) const + { + auto r = detail::compute_slice(shape_, strides_, first, rest...); + return {data_ + r.ptr_offset, std::move(r.shape), std::move(r.strides)}; + } + + //-------------------------------------------------------------------------- + // Assignment operators + + //! Copy assignment: element-wise deep copy (writes through data pointer). + //! Without this, the compiler's implicit copy assignment just copies the + //! View metadata (pointer, shape, strides) instead of the viewed data. + View& operator=(const View& other) + { + size_t n = size(); + for (size_t i = 0; i < n; ++i) + data_[flat_to_offset(i)] = other[i]; + return *this; + } + + //! Fill all elements with a scalar + View& operator=(T val) + { + size_t n = size(); + for (size_t i = 0; i < n; ++i) + data_[flat_to_offset(i)] = val; + return *this; + } + + //! Assignment from initializer_list (for 1D views) + View& operator=(std::initializer_list vals) + { + auto it = vals.begin(); + for (size_t i = 0; i < size() && it != vals.end(); ++i, ++it) + data_[flat_to_offset(i)] = *it; + return *this; + } + + //! Assignment from another View + template + View& operator=(const View& other) + { + size_t n = size(); + for (size_t i = 0; i < n; ++i) + data_[flat_to_offset(i)] = other[i]; + return *this; + } + + //! Assignment from Tensor (forward-declared, defined after Tensor) + template + View& operator=(const Tensor& other); + + //! Compound addition from Tensor (forward-declared, defined after Tensor) + template + View& operator+=(const Tensor& o); + + //! Compound multiply by scalar + View& operator*=(T val) + { + size_t n = size(); + for (size_t i = 0; i < n; ++i) + data_[flat_to_offset(i)] *= val; + return *this; + } + + //! Compound divide by scalar + View& operator/=(T val) + { + size_t n = size(); + for (size_t i = 0; i < n; ++i) + data_[flat_to_offset(i)] /= val; + return *this; + } + + //-------------------------------------------------------------------------- + // Reductions + + //! Sum of all elements + T sum() const + { + // remove_const needed so accumulator is mutable when T is const-qualified + std::remove_const_t s = 0; + size_t n = size(); + for (size_t i = 0; i < n; ++i) + s += data_[flat_to_offset(i)]; + return s; + } + + //-------------------------------------------------------------------------- + // Iterators + // + // Lightweight row-major iterator parameterized on pointer type (Ptr). + // Stores a flat logical position and converts to a physical offset on each + // dereference via divmod over shape/strides. For contiguous 1D views (the + // common case) the divmod chain reduces to a single multiply-by-1, which + // the compiler optimizes away. + // + // view_iterator = mutable iterator (from non-const View) + // view_iterator = read-only iterator (from const View) + + template + class view_iterator { + Ptr base_; + size_t count_; + const size_t* shape_; + const size_t* strides_; + size_t ndim_; + + public: + using iterator_category = std::random_access_iterator_tag; + using value_type = std::remove_const_t; + using difference_type = std::ptrdiff_t; + using pointer = Ptr; + using reference = decltype(*std::declval()); + + view_iterator(Ptr base, size_t count, const View* v) + : base_(base), count_(count), shape_(v->shape_.data()), + strides_(v->strides_.data()), ndim_(v->shape_.size()) + {} + + reference operator*() const { return base_[offset()]; } + reference operator[](difference_type n) const + { + return base_[offset_of(count_ + n)]; + } + view_iterator& operator++() + { + ++count_; + return *this; + } + view_iterator operator++(int) + { + auto tmp = *this; + ++count_; + return tmp; + } + view_iterator& operator--() + { + --count_; + return *this; + } + view_iterator operator+(difference_type n) const + { + auto tmp = *this; + tmp.count_ += n; + return tmp; + } + view_iterator operator-(difference_type n) const + { + auto tmp = *this; + tmp.count_ -= n; + return tmp; + } + difference_type operator-(const view_iterator& o) const + { + return static_cast(count_) - + static_cast(o.count_); + } + view_iterator& operator+=(difference_type n) + { + count_ += n; + return *this; + } + view_iterator& operator-=(difference_type n) + { + count_ -= n; + return *this; + } + bool operator==(const view_iterator& o) const { return count_ == o.count_; } + bool operator!=(const view_iterator& o) const { return count_ != o.count_; } + bool operator<(const view_iterator& o) const { return count_ < o.count_; } + bool operator>(const view_iterator& o) const { return count_ > o.count_; } + bool operator<=(const view_iterator& o) const { return count_ <= o.count_; } + bool operator>=(const view_iterator& o) const { return count_ >= o.count_; } + friend view_iterator operator+(difference_type n, const view_iterator& it) + { + return it + n; + } + + private: + size_t offset() const { return offset_of(count_); } + size_t offset_of(size_t flat) const + { + size_t off = 0; + for (int d = static_cast(ndim_) - 1; d >= 0; --d) { + off += (flat % shape_[d]) * strides_[d]; + flat /= shape_[d]; + } + return off; + } + }; + + using iterator = view_iterator; + using const_iterator = view_iterator; + + iterator begin() { return {data_, 0, this}; } + iterator end() { return {data_, size(), this}; } + const_iterator begin() const { return cbegin(); } + const_iterator end() const { return cend(); } + const_iterator cbegin() const { return {data_, 0, this}; } + const_iterator cend() const { return {data_, size(), this}; } + +private: + //! Convert a logical flat index (row-major) to a physical element offset + size_t flat_to_offset(size_t flat) const + { + size_t off = 0; + for (int d = static_cast(shape_.size()) - 1; d >= 0; --d) { + off += (flat % shape_[d]) * strides_[d]; + flat /= shape_[d]; + } + return off; + } + + T* data_; + vector shape_; + vector strides_; +}; + +//============================================================================== +// Tensor: dynamic-rank N-dimensional tensor. +// +// Stores elements in a contiguous row-major vector> +// with a dynamic shape. +//============================================================================== + +template +class Tensor { +public: + using value_type = T; + using stored_type = storage_type; + using iterator = typename vector::iterator; + using const_iterator = typename vector::const_iterator; + + //-------------------------------------------------------------------------- + // Constructors + + Tensor() = default; + + //! Construct with shape (uninitialized for arithmetic types via vector + //! resize) + explicit Tensor(vector shape) + : shape_(std::move(shape)), data_(compute_size()) + {} + + //! Construct with shape and fill value + Tensor(vector shape, T fill) + : shape_(std::move(shape)), data_(compute_size(), fill) + {} + + //! Construct from initializer_list shape + explicit Tensor(std::initializer_list shape) + : shape_(shape), data_(compute_size()) + {} + + //! Construct from initializer_list shape with fill + Tensor(std::initializer_list shape, T fill) + : shape_(shape), data_(compute_size(), fill) + {} + + //! 1D copy from raw pointer + count + Tensor(const T* ptr, size_t count) : shape_({count}), data_(ptr, ptr + count) + {} + + //! Copy from View (preserves view's shape) + template + explicit Tensor(const View& v) : shape_(v.shape_vec()) + { + size_t n = v.size(); + data_.resize(n); + for (size_t i = 0; i < n; ++i) + data_[i] = v[i]; + } + + //-------------------------------------------------------------------------- + // Assignment + + //! Assignment from View + template + Tensor& operator=(const View& v) + { + shape_ = v.shape_vec(); + size_t n = v.size(); + data_.resize(n); + for (size_t i = 0; i < n; ++i) + data_[i] = v[i]; + return *this; + } + + //! Assignment from initializer_list of values (1D) + Tensor& operator=(std::initializer_list vals) + { + shape_ = {vals.size()}; + data_.assign(vals.begin(), vals.end()); + return *this; + } + + //-------------------------------------------------------------------------- + // Accessors + + stored_type* data() { return data_.data(); } + const stored_type* data() const { return data_.data(); } + size_t size() const { return data_.size(); } + const vector& shape() const { return shape_; } + size_t shape(size_t dim) const + { + return dim < shape_.size() ? shape_[dim] : 0; + } + size_t ndim() const { return shape_.size(); } + bool empty() const { return data_.empty(); } + + //-------------------------------------------------------------------------- + // Indexing (row-major) + + template + stored_type& operator()(Indices... indices) + { + const size_t idx[] = {static_cast(indices)...}; + size_t off = 0; + for (size_t d = 0; d < sizeof...(Indices); ++d) + off = off * shape_[d] + idx[d]; + return data_[off]; + } + + template + const stored_type& operator()(Indices... indices) const + { + const size_t idx[] = {static_cast(indices)...}; + size_t off = 0; + for (size_t d = 0; d < sizeof...(Indices); ++d) + off = off * shape_[d] + idx[d]; + return data_[off]; + } + + stored_type& operator[](size_t i) { return data_[i]; } + const stored_type& operator[](size_t i) const { return data_[i]; } + + //! First and last element + stored_type& front() { return data_.front(); } + const stored_type& front() const { return data_.front(); } + stored_type& back() { return data_.back(); } + const stored_type& back() const { return data_.back(); } + + //-------------------------------------------------------------------------- + // Iterators + + iterator begin() { return data_.begin(); } + iterator end() { return data_.end(); } + const_iterator begin() const { return data_.begin(); } + const_iterator end() const { return data_.end(); } + const_iterator cbegin() const { return data_.cbegin(); } + const_iterator cend() const { return data_.cend(); } + + //-------------------------------------------------------------------------- + // Mutation + + void resize(const vector& shape) + { + shape_ = shape; + data_.resize(compute_size()); + } + + void resize(std::initializer_list shape) + { + shape_.assign(shape.begin(), shape.end()); + data_.resize(compute_size()); + } + + void reshape(const vector& new_shape) { shape_ = new_shape; } + + void fill(T val) { std::fill(data_.begin(), data_.end(), val); } + + //-------------------------------------------------------------------------- + // View accessors + + //! Fix one axis at a given index, returning an (N-1)-dimensional view + //! Multi-axis slice. Each argument corresponds to one axis and is either: + //! - an integer (fixes that axis, rank-reducing) + //! - All (keeps entire axis) + //! - Range (keeps sub-range along that axis) + //! Example: t.slice(0, all, range(2, 5)) == numpy t[0, :, 2:5] + template + View slice(First first, Rest... rest) + { + auto strides = compute_strides(); + auto r = detail::compute_slice(shape_, strides, first, rest...); + return { + data_.data() + r.ptr_offset, std::move(r.shape), std::move(r.strides)}; + } + + template + View slice(First first, Rest... rest) const + { + auto strides = compute_strides(); + auto r = detail::compute_slice(shape_, strides, first, rest...); + return { + data_.data() + r.ptr_offset, std::move(r.shape), std::move(r.strides)}; + } + + //! Flat 1D view of all elements + View flat() + { + return {data_.data(), {data_.size()}, {size_t(1)}}; + } + View flat() const + { + return {data_.data(), {data_.size()}, {size_t(1)}}; + } + + //-------------------------------------------------------------------------- + // Reductions and transforms + + //! Sum of all elements + T sum() const + { + T s = T(0); + for (size_t i = 0; i < data_.size(); ++i) + s += data_[i]; + return s; + } + + //! Sum along an axis, reducing rank by 1 (defined out-of-line below) + Tensor sum(size_t axis) const; + + //! Product of all elements + T prod() const + { + T p = T(1); + for (size_t i = 0; i < data_.size(); ++i) + p *= data_[i]; + return p; + } + + //! True if any element is nonzero + bool any() const + { + for (size_t i = 0; i < data_.size(); ++i) + if (data_[i]) + return true; + return false; + } + + //! True if all elements are nonzero + bool all() const + { + for (size_t i = 0; i < data_.size(); ++i) + if (!data_[i]) + return false; + return true; + } + + //! Flat index of the minimum element + size_t argmin() const + { + return static_cast(std::distance(data_.data(), + std::min_element(data_.data(), data_.data() + data_.size()))); + } + + //! Reverse element order along an axis (e.g. flip(0) reverses rows) + Tensor flip(size_t axis) const + { + size_t outer_size = 1; + for (size_t d = 0; d < axis; ++d) + outer_size *= shape_[d]; + size_t axis_size = shape_[axis]; + size_t inner_size = 1; + for (size_t d = axis + 1; d < shape_.size(); ++d) + inner_size *= shape_[d]; + + Tensor r(shape_); + for (size_t o = 0; o < outer_size; ++o) + for (size_t a = 0; a < axis_size; ++a) + for (size_t i = 0; i < inner_size; ++i) + r.data_[(o * axis_size + (axis_size - 1 - a)) * inner_size + i] = + data_[(o * axis_size + a) * inner_size + i]; + return r; + } + + //-------------------------------------------------------------------------- + // Operators + + Tensor& operator+=(T val) + { + for (auto& x : data_) + x += val; + return *this; + } + Tensor& operator-=(T val) + { + for (auto& x : data_) + x -= val; + return *this; + } + Tensor& operator*=(T val) + { + for (auto& x : data_) + x *= val; + return *this; + } + Tensor& operator/=(T val) + { + for (auto& x : data_) + x /= val; + return *this; + } + Tensor& operator+=(const Tensor& o) + { + for (size_t i = 0; i < data_.size(); ++i) + data_[i] += o.data_[i]; + return *this; + } + + Tensor operator+(const Tensor& o) const + { + Tensor r(shape_); + for (size_t i = 0; i < data_.size(); ++i) + r.data_[i] = data_[i] + o.data_[i]; + return r; + } + Tensor operator-(const Tensor& o) const + { + Tensor r(shape_); + for (size_t i = 0; i < data_.size(); ++i) + r.data_[i] = data_[i] - o.data_[i]; + return r; + } + Tensor operator/(const Tensor& o) const + { + Tensor r(shape_); + for (size_t i = 0; i < data_.size(); ++i) + r.data_[i] = data_[i] / o.data_[i]; + return r; + } + + Tensor operator+(T val) const + { + Tensor r(shape_); + for (size_t i = 0; i < data_.size(); ++i) + r.data_[i] = data_[i] + val; + return r; + } + Tensor operator-(T val) const + { + Tensor r(shape_); + for (size_t i = 0; i < data_.size(); ++i) + r.data_[i] = data_[i] - val; + return r; + } + Tensor operator*(T val) const + { + Tensor r(shape_); + for (size_t i = 0; i < data_.size(); ++i) + r.data_[i] = data_[i] * val; + return r; + } + + Tensor operator<=(T val) const + { + Tensor r(shape_); + for (size_t i = 0; i < data_.size(); ++i) + r.data()[i] = data_[i] <= val; + return r; + } + Tensor operator<(T val) const + { + Tensor r(shape_); + for (size_t i = 0; i < data_.size(); ++i) + r.data()[i] = data_[i] < val; + return r; + } + Tensor operator>=(T val) const + { + Tensor r(shape_); + for (size_t i = 0; i < data_.size(); ++i) + r.data()[i] = data_[i] >= val; + return r; + } + Tensor operator>(T val) const + { + Tensor r(shape_); + for (size_t i = 0; i < data_.size(); ++i) + r.data()[i] = data_[i] > val; + return r; + } + Tensor operator<(const Tensor& o) const + { + Tensor r(shape_); + for (size_t i = 0; i < data_.size(); ++i) + r.data()[i] = data_[i] < o.data_[i]; + return r; + } + +private: + size_t compute_size() const + { + size_t s = 1; + for (auto d : shape_) + s *= d; + return s; + } + + //! Compute row-major strides from shape + vector compute_strides() const + { + vector strides(shape_.size()); + if (!shape_.empty()) { + strides.back() = 1; + for (int d = static_cast(shape_.size()) - 2; d >= 0; --d) + strides[d] = strides[d + 1] * shape_[d + 1]; + } + return strides; + } + + //-------------------------------------------------------------------------- + // Data members + + vector shape_; + vector> data_; +}; + +//============================================================================== +// Non-member operators (scalar op tensor) +//============================================================================== + +template +Tensor operator*(T val, const Tensor& arr) +{ + return arr * val; +} + +template +Tensor operator+(T val, const Tensor& arr) +{ + return arr + val; +} + +// Mixed-type arithmetic: Tensor op Tensor -> Tensor +// A SFINAE guard is used here, as without !is_same Tensor * Tensor +// would be ambiguous between the member operator* and this non-member function. +template::value>> +Tensor operator*(const Tensor& a, const Tensor& b) +{ + Tensor r(a.shape()); + for (size_t i = 0; i < a.size(); ++i) + r.data()[i] = + static_cast(a.data()[i]) * static_cast(b.data()[i]); + return r; +} + +// Same SFINAE guard as operator* above. +template::value>> +Tensor operator/(const Tensor& a, const Tensor& b) +{ + Tensor r(a.shape()); + for (size_t i = 0; i < a.size(); ++i) + r.data()[i] = + static_cast(a.data()[i]) / static_cast(b.data()[i]); + return r; +} + +//============================================================================== +// Out-of-line method definitions (require complete types) +//============================================================================== + +template +template +View& View::operator=(const Tensor& other) +{ + size_t n = size(); + for (size_t i = 0; i < n; ++i) + data_[flat_to_offset(i)] = static_cast(other.data()[i]); + return *this; +} + +template +template +View& View::operator+=(const Tensor& o) +{ + size_t n = size(); + for (size_t i = 0; i < n; ++i) + data_[flat_to_offset(i)] += o.data()[i]; + return *this; +} + +template +Tensor Tensor::sum(size_t axis) const +{ + // Build output shape (all dims except the summed axis) + vector out_shape; + for (size_t d = 0; d < shape_.size(); ++d) + if (d != axis) + out_shape.push_back(shape_[d]); + + // Split dimensions into three zones: outer | axis | inner + size_t outer_size = 1; + for (size_t d = 0; d < axis; ++d) + outer_size *= shape_[d]; + size_t axis_size = shape_[axis]; + size_t inner_size = 1; + for (size_t d = axis + 1; d < shape_.size(); ++d) + inner_size *= shape_[d]; + + Tensor result(out_shape, T(0)); + for (size_t o = 0; o < outer_size; ++o) + for (size_t a = 0; a < axis_size; ++a) + for (size_t i = 0; i < inner_size; ++i) + result.data()[o * inner_size + i] += + data_[(o * axis_size + a) * inner_size + i]; + + return result; +} + +//============================================================================== +// StaticTensor2D: compile-time fixed 2D tensor. +//============================================================================== + +template +class StaticTensor2D { +public: + using value_type = T; + + //-------------------------------------------------------------------------- + // Indexing + + //! Templated to accept enum class indices (e.g. GlobalTally, TallyResult) + //! which don't implicitly convert to integer types. + template + T& operator()(I0 i, I1 j) + { + return data_[static_cast(i) * C + static_cast(j)]; + } + template + const T& operator()(I0 i, I1 j) const + { + return data_[static_cast(i) * C + static_cast(j)]; + } + + //-------------------------------------------------------------------------- + // Accessors + + T* data() { return data_; } + const T* data() const { return data_; } + constexpr size_t size() const { return R * C; } + std::array shape() const { return {R, C}; } + + //-------------------------------------------------------------------------- + // Mutation + + void fill(T val) { std::fill(data_, data_ + R * C, val); } + + //-------------------------------------------------------------------------- + // Iterators + + T* begin() { return data_; } + T* end() { return data_ + R * C; } + const T* begin() const { return data_; } + const T* end() const { return data_ + R * C; } + + //-------------------------------------------------------------------------- + // View accessors + + //! Multi-axis slice (same interface as Tensor/View). + template + View slice(First first, Rest... rest) + { + vector sh = {R, C}; + vector st = {C, 1}; + auto r = detail::compute_slice(sh, st, first, rest...); + return {data_ + r.ptr_offset, std::move(r.shape), std::move(r.strides)}; + } + template + View slice(First first, Rest... rest) const + { + vector sh = {R, C}; + vector st = {C, 1}; + auto r = detail::compute_slice(sh, st, first, rest...); + return {data_ + r.ptr_offset, std::move(r.shape), std::move(r.strides)}; + } + + //! Flat view (1D, contiguous) + View flat() { return {data_, {R * C}, {size_t(1)}}; } + View flat() const { return {data_, {R * C}, {size_t(1)}}; } + +private: + //-------------------------------------------------------------------------- + // Data members + + T data_[R * C] = {}; +}; + +//============================================================================== +// Non-member functions +//============================================================================== + +template +Tensor zeros(std::initializer_list shape) +{ + vector s(shape); + return Tensor(std::move(s), T(0)); +} + +template +Tensor zeros(const vector& shape) +{ + return Tensor(shape, T(0)); +} + +template +Tensor ones(std::initializer_list shape) +{ + vector s(shape); + return Tensor(std::move(s), T(1)); +} + +template +Tensor ones(const vector& shape) +{ + return Tensor(shape, T(1)); +} + +template +Tensor zeros_like(const Tensor& o) +{ + return Tensor(o.shape(), T(0)); +} + +template +Tensor full_like(const Tensor& o, V val) +{ + return Tensor(o.shape(), static_cast(val)); +} + +//! Return a 1D tensor of n evenly spaced values from start to stop (inclusive) +template +Tensor linspace(T start, T stop, size_t n) +{ + Tensor result({n}); + if (n < 2) { + result[0] = start; + return result; + } + for (size_t i = 0; i < n; ++i) { + result[i] = + start + static_cast(i) * (stop - start) / static_cast(n - 1); + } + return result; +} + +//! Concatenate two 1D tensors end-to-end +template +Tensor concatenate(const Tensor& a, const Tensor& b) +{ + size_t total = a.size() + b.size(); + Tensor result({total}); + std::copy(a.data(), a.data() + a.size(), result.data()); + std::copy(b.data(), b.data() + b.size(), result.data() + a.size()); + return result; +} + +//! Element-wise natural logarithm +template +Tensor log(const Tensor& a) +{ + Tensor r(a.shape()); + for (size_t i = 0; i < a.size(); ++i) + r.data()[i] = std::log(a.data()[i]); + return r; +} + +//! Element-wise absolute value +template +Tensor abs(const Tensor& a) +{ + Tensor r(a.shape()); + for (size_t i = 0; i < a.size(); ++i) + r.data()[i] = std::abs(a.data()[i]); + return r; +} + +//! Element-wise conditional: select from true_val where cond is true, +//! otherwise use false_val +template +Tensor where( + const Tensor& cond, const Tensor& true_val, V false_val) +{ + Tensor r(cond.shape()); + for (size_t i = 0; i < cond.size(); ++i) + r.data()[i] = + cond.data()[i] ? true_val.data()[i] : static_cast(false_val); + return r; +} + +//! Replace NaN/Inf values with finite substitutes +template +Tensor nan_to_num(const Tensor& a, T nan_val = T(0), + T posinf_val = std::numeric_limits::max(), + T neginf_val = std::numeric_limits::lowest()) +{ + Tensor r(a.shape()); + for (size_t i = 0; i < a.size(); ++i) { + T val = a.data()[i]; + if (std::isnan(val)) + r.data()[i] = nan_val; + else if (std::isinf(val)) + r.data()[i] = val > 0 ? posinf_val : neginf_val; + else + r.data()[i] = val; + } + return r; +} + +//============================================================================== +// Type traits +//============================================================================== + +//! Type trait that is true for Tensor and StaticTensor2D. +//! Used by hdf5_interface.h to select the correct write_dataset overload. +template +struct is_tensor : std::false_type {}; + +template +struct is_tensor> : std::true_type {}; + +template +struct is_tensor> : std::true_type {}; + +} // namespace tensor +} // namespace openmc + +#endif // OPENMC_TENSOR_H diff --git a/include/openmc/thermal.h b/include/openmc/thermal.h index de0767d0af..86254b9231 100644 --- a/include/openmc/thermal.h +++ b/include/openmc/thermal.h @@ -5,7 +5,7 @@ #include #include -#include "xtensor/xtensor.hpp" +#include "openmc/tensor.h" #include "openmc/angle_energy.h" #include "openmc/endf.h" diff --git a/include/openmc/urr.h b/include/openmc/urr.h index 1e60371584..d40c2aff7e 100644 --- a/include/openmc/urr.h +++ b/include/openmc/urr.h @@ -3,7 +3,7 @@ #ifndef OPENMC_URR_H #define OPENMC_URR_H -#include "xtensor/xtensor.hpp" +#include "openmc/tensor.h" #include "openmc/constants.h" #include "openmc/hdf5_interface.h" @@ -40,11 +40,11 @@ public: * below, obviously, values of the CDF are stored. For the xs_values * variable, the columns line up with the index of cdf_values. */ - xt::xtensor cdf_values_; // Note: must be row major! - xt::xtensor xs_values_; + tensor::Tensor cdf_values_; // Note: must be row major! + tensor::Tensor xs_values_; // Number of points in the CDF - auto n_cdf() const { return cdf_values_.shape()[1]; } + auto n_cdf() const { return cdf_values_.shape(1); } //! \brief Load the URR data from the provided HDF5 group explicit UrrData(hid_t group_id); diff --git a/include/openmc/volume_calc.h b/include/openmc/volume_calc.h index fa8d3d65ec..9d3f1d0261 100644 --- a/include/openmc/volume_calc.h +++ b/include/openmc/volume_calc.h @@ -12,8 +12,8 @@ #include "openmc/tallies/trigger.h" #include "openmc/vector.h" +#include "openmc/tensor.h" #include "pugixml.hpp" -#include "xtensor/xtensor.hpp" #ifdef _OPENMP #include #endif diff --git a/include/openmc/weight_windows.h b/include/openmc/weight_windows.h index 0d7435ca2a..97abcfe82e 100644 --- a/include/openmc/weight_windows.h +++ b/include/openmc/weight_windows.h @@ -143,10 +143,10 @@ public: const vector& energy_bounds() const { return energy_bounds_; } - void set_bounds(const xt::xtensor& lower_ww_bounds, - const xt::xtensor& upper_bounds); + void set_bounds(const tensor::Tensor& lower_ww_bounds, + const tensor::Tensor& upper_bounds); - void set_bounds(const xt::xtensor& lower_bounds, double ratio); + void set_bounds(const tensor::Tensor& lower_bounds, double ratio); void set_bounds( span lower_bounds, span upper_bounds); @@ -182,11 +182,11 @@ public: const std::unique_ptr& mesh() const { return model::meshes[mesh_idx_]; } - const xt::xtensor& lower_ww_bounds() const { return lower_ww_; } - xt::xtensor& lower_ww_bounds() { return lower_ww_; } + const tensor::Tensor& lower_ww_bounds() const { return lower_ww_; } + tensor::Tensor& lower_ww_bounds() { return lower_ww_; } - const xt::xtensor& upper_ww_bounds() const { return upper_ww_; } - xt::xtensor& upper_ww_bounds() { return upper_ww_; } + const tensor::Tensor& upper_ww_bounds() const { return upper_ww_; } + tensor::Tensor& upper_ww_bounds() { return upper_ww_; } ParticleType particle_type() const { return particle_type_; } @@ -197,9 +197,9 @@ private: int64_t index_; //!< Index into weight windows vector ParticleType particle_type_; //!< Particle type to apply weight windows to vector energy_bounds_; //!< Energy boundaries [eV] - xt::xtensor lower_ww_; //!< Lower weight window bounds (shape: + tensor::Tensor lower_ww_; //!< Lower weight window bounds (shape: //!< energy_bins, mesh_bins (k, j, i)) - xt::xtensor + tensor::Tensor upper_ww_; //!< Upper weight window bounds (shape: energy_bins, mesh_bins) double survival_ratio_ {3.0}; //!< Survival weight ratio double max_lb_ratio_ {1.0}; //!< Maximum lower bound to particle weight ratio diff --git a/include/openmc/wmp.h b/include/openmc/wmp.h index 6a4abd8671..5cc04e5959 100644 --- a/include/openmc/wmp.h +++ b/include/openmc/wmp.h @@ -2,7 +2,7 @@ #define OPENMC_WMP_H #include "hdf5.h" -#include "xtensor/xtensor.hpp" +#include "openmc/tensor.h" #include #include @@ -78,9 +78,9 @@ public: int fit_order_; //!< Order of the fit bool fissionable_; //!< Is the nuclide fissionable? vector window_info_; // Information about a window - xt::xtensor + tensor::Tensor curvefit_; // Curve fit coefficients (window, poly order, reaction) - xt::xtensor, 2> data_; //!< Poles and residues + tensor::Tensor> data_; //!< Poles and residues // Constant data static constexpr int MAX_POLY_COEFFICIENTS = diff --git a/include/openmc/xml_interface.h b/include/openmc/xml_interface.h index f49613ecde..17a34e5c77 100644 --- a/include/openmc/xml_interface.h +++ b/include/openmc/xml_interface.h @@ -5,9 +5,8 @@ #include // for stringstream #include +#include "openmc/tensor.h" #include "pugixml.hpp" -#include "xtensor/xadapt.hpp" -#include "xtensor/xarray.hpp" #include "openmc/position.h" #include "openmc/vector.h" @@ -42,12 +41,11 @@ vector get_node_array( } template -xt::xarray get_node_xarray( +tensor::Tensor get_node_tensor( pugi::xml_node node, const char* name, bool lowercase = false) { vector v = get_node_array(node, name, lowercase); - vector shape = {v.size()}; - return xt::adapt(v, shape); + return tensor::Tensor(v.data(), v.size()); } std::vector get_node_position_array( diff --git a/include/openmc/xsdata.h b/include/openmc/xsdata.h index feafde68dd..c9dbde986b 100644 --- a/include/openmc/xsdata.h +++ b/include/openmc/xsdata.h @@ -4,7 +4,7 @@ #ifndef OPENMC_XSDATA_H #define OPENMC_XSDATA_H -#include "xtensor/xtensor.hpp" +#include "openmc/tensor.h" #include "openmc/hdf5_interface.h" #include "openmc/memory.h" @@ -69,26 +69,26 @@ private: public: // The following quantities have the following dimensions: // [angle][incoming group] - xt::xtensor total; - xt::xtensor absorption; - xt::xtensor nu_fission; - xt::xtensor prompt_nu_fission; - xt::xtensor kappa_fission; - xt::xtensor fission; - xt::xtensor inverse_velocity; + tensor::Tensor total; + tensor::Tensor absorption; + tensor::Tensor nu_fission; + tensor::Tensor prompt_nu_fission; + tensor::Tensor kappa_fission; + tensor::Tensor fission; + tensor::Tensor inverse_velocity; // decay_rate has the following dimensions: // [angle][delayed group] - xt::xtensor decay_rate; + tensor::Tensor decay_rate; // delayed_nu_fission has the following dimensions: // [angle][delayed group][incoming group] - xt::xtensor delayed_nu_fission; + tensor::Tensor delayed_nu_fission; // chi_prompt has the following dimensions: // [angle][incoming group][outgoing group] - xt::xtensor chi_prompt; + tensor::Tensor chi_prompt; // chi_delayed has the following dimensions: // [angle][incoming group][outgoing group][delayed group] - xt::xtensor chi_delayed; + tensor::Tensor chi_delayed; // scatter has the following dimensions: [angle] vector> scatter; diff --git a/src/bank.cpp b/src/bank.cpp index 33790379b8..5afdc2ecab 100644 --- a/src/bank.cpp +++ b/src/bank.cpp @@ -7,6 +7,7 @@ #include "openmc/vector.h" #include +#include namespace openmc { diff --git a/src/bremsstrahlung.cpp b/src/bremsstrahlung.cpp index d77066fb0e..ec1088101f 100644 --- a/src/bremsstrahlung.cpp +++ b/src/bremsstrahlung.cpp @@ -6,7 +6,7 @@ #include "openmc/search.h" #include "openmc/settings.h" -#include "xtensor/xmath.hpp" +#include "openmc/tensor.h" namespace openmc { @@ -16,8 +16,8 @@ namespace openmc { namespace data { -xt::xtensor ttb_e_grid; -xt::xtensor ttb_k_grid; +tensor::Tensor ttb_e_grid; +tensor::Tensor ttb_k_grid; vector ttb; } // namespace data diff --git a/src/cmfd_solver.cpp b/src/cmfd_solver.cpp index 943042f67e..714a5bf3ac 100644 --- a/src/cmfd_solver.cpp +++ b/src/cmfd_solver.cpp @@ -5,7 +5,7 @@ #ifdef _OPENMP #include #endif -#include "xtensor/xtensor.hpp" +#include "openmc/tensor.h" #include "openmc/bank.h" #include "openmc/capi.h" @@ -36,7 +36,7 @@ double spectral; int nx, ny, nz, ng; -xt::xtensor indexmap; +tensor::Tensor indexmap; int use_all_threads; @@ -79,15 +79,14 @@ int get_cmfd_energy_bin(const double E) // COUNT_BANK_SITES bins fission sites according to CMFD mesh and energy //============================================================================== -xt::xtensor count_bank_sites( - xt::xtensor& bins, bool* outside) +tensor::Tensor count_bank_sites( + tensor::Tensor& bins, bool* outside) { // Determine shape of array for counts std::size_t cnt_size = cmfd::nx * cmfd::ny * cmfd::nz * cmfd::ng; - vector cnt_shape = {cnt_size}; // Create array of zeros - xt::xarray cnt {cnt_shape, 0.0}; + tensor::Tensor cnt = tensor::zeros({cnt_size}); bool outside_ = false; auto bank_size = simulation::source_bank.size(); @@ -113,29 +112,22 @@ xt::xtensor count_bank_sites( bins[i] = mesh_bin * cmfd::ng + energy_bin; } - // Create copy of count data. Since ownership will be acquired by xtensor, - // std::allocator must be used to avoid Valgrind mismatched free() / delete - // warnings. int total = cnt.size(); - double* cnt_reduced = std::allocator {}.allocate(total); + tensor::Tensor counts = tensor::zeros({cnt_size}); #ifdef OPENMC_MPI // collect values from all processors MPI_Reduce( - cnt.data(), cnt_reduced, total, MPI_DOUBLE, MPI_SUM, 0, mpi::intracomm); + cnt.data(), counts.data(), total, MPI_DOUBLE, MPI_SUM, 0, mpi::intracomm); // Check if there were sites outside the mesh for any processor MPI_Reduce(&outside_, outside, 1, MPI_C_BOOL, MPI_LOR, 0, mpi::intracomm); #else - std::copy(cnt.data(), cnt.data() + total, cnt_reduced); + std::copy(cnt.data(), cnt.data() + total, counts.data()); *outside = outside_; #endif - // Adapt reduced values in array back into an xarray - auto arr = xt::adapt(cnt_reduced, total, xt::acquire_ownership(), cnt_shape); - xt::xarray counts = arr; - return counts; } @@ -151,19 +143,19 @@ extern "C" void openmc_cmfd_reweight( std::size_t src_size = cmfd::nx * cmfd::ny * cmfd::nz * cmfd::ng; // count bank sites for CMFD mesh, store bins in bank_bins for reweighting - xt::xtensor bank_bins({bank_size}, 0); + tensor::Tensor bank_bins = tensor::zeros({bank_size}); bool sites_outside; - xt::xtensor sourcecounts = + tensor::Tensor sourcecounts = count_bank_sites(bank_bins, &sites_outside); // Compute CMFD weightfactors - xt::xtensor weightfactors = xt::xtensor({src_size}, 1.); + tensor::Tensor weightfactors = tensor::ones({src_size}); if (mpi::master) { if (sites_outside) { fatal_error("Source sites outside of the CMFD mesh"); } - double norm = xt::sum(sourcecounts)() / cmfd::norm; + double norm = sourcecounts.sum() / cmfd::norm; for (int i = 0; i < src_size; i++) { if (sourcecounts[i] > 0 && cmfd_src[i] > 0) { weightfactors[i] = cmfd_src[i] * norm / sourcecounts[i]; @@ -561,7 +553,7 @@ void free_memory_cmfd() cmfd::indices.clear(); cmfd::egrid.clear(); - // Resize xtensors to be empty + // Resize tensors to be empty cmfd::indexmap.resize({0}); // Set pointers to null diff --git a/src/cross_sections.cpp b/src/cross_sections.cpp index b1bfde03d1..ec9da5b8ab 100644 --- a/src/cross_sections.cpp +++ b/src/cross_sections.cpp @@ -254,7 +254,7 @@ void read_ce_cross_sections(const vector>& nuc_temps, if (settings::photon_transport && settings::electron_treatment == ElectronTreatment::TTB) { // Take logarithm of energies since they are log-log interpolated - data::ttb_e_grid = xt::log(data::ttb_e_grid); + data::ttb_e_grid = tensor::log(data::ttb_e_grid); } // Show minimum/maximum temperature diff --git a/src/distribution_angle.cpp b/src/distribution_angle.cpp index 50f1aca112..3433f269e3 100644 --- a/src/distribution_angle.cpp +++ b/src/distribution_angle.cpp @@ -2,8 +2,7 @@ #include // for abs, copysign -#include "xtensor/xarray.hpp" -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include "openmc/endf.h" #include "openmc/hdf5_interface.h" @@ -30,7 +29,7 @@ AngleDistribution::AngleDistribution(hid_t group) hid_t dset = open_dataset(group, "mu"); read_attribute(dset, "offsets", offsets); read_attribute(dset, "interpolation", interp); - xt::xarray temp; + tensor::Tensor temp; read_dataset(dset, temp); close_dataset(dset); @@ -41,13 +40,13 @@ AngleDistribution::AngleDistribution(hid_t group) if (i < n_energy - 1) { n = offsets[i + 1] - j; } else { - n = temp.shape()[1] - j; + n = temp.shape(1) - j; } // Create and initialize tabular distribution - auto xs = xt::view(temp, 0, xt::range(j, j + n)); - auto ps = xt::view(temp, 1, xt::range(j, j + n)); - auto cs = xt::view(temp, 2, xt::range(j, j + n)); + tensor::View xs = temp.slice(0, tensor::range(j, j + n)); + tensor::View ps = temp.slice(1, tensor::range(j, j + n)); + tensor::View cs = temp.slice(2, tensor::range(j, j + n)); vector x {xs.begin(), xs.end()}; vector p {ps.begin(), ps.end()}; vector c {cs.begin(), cs.end()}; diff --git a/src/distribution_energy.cpp b/src/distribution_energy.cpp index a4a5ce9e1b..2f8e6cf1a9 100644 --- a/src/distribution_energy.cpp +++ b/src/distribution_energy.cpp @@ -4,7 +4,7 @@ #include // for size_t #include // for back_inserter -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include "openmc/endf.h" #include "openmc/hdf5_interface.h" @@ -60,11 +60,11 @@ ContinuousTabular::ContinuousTabular(hid_t group) hid_t dset = open_dataset(group, "energy"); // Get interpolation parameters - xt::xarray temp; + tensor::Tensor temp; read_attribute(dset, "interpolation", temp); - auto temp_b = xt::view(temp, 0); // view of breakpoints - auto temp_i = xt::view(temp, 1); // view of interpolation parameters + tensor::View temp_b = temp.slice(0); // breakpoints + tensor::View temp_i = temp.slice(1); // interpolation parameters std::copy(temp_b.begin(), temp_b.end(), std::back_inserter(breakpoints_)); for (const auto i : temp_i) @@ -85,7 +85,7 @@ ContinuousTabular::ContinuousTabular(hid_t group) read_attribute(dset, "interpolation", interp); read_attribute(dset, "n_discrete_lines", n_discrete); - xt::xarray eout; + tensor::Tensor eout; read_dataset(dset, eout); close_dataset(dset); @@ -96,7 +96,7 @@ ContinuousTabular::ContinuousTabular(hid_t group) if (i < n_energy - 1) { n = offsets[i + 1] - j; } else { - n = eout.shape()[1] - j; + n = eout.shape(1) - j; } // Assign interpolation scheme and number of discrete lines @@ -105,15 +105,15 @@ ContinuousTabular::ContinuousTabular(hid_t group) d.n_discrete = n_discrete[i]; // Copy data - d.e_out = xt::view(eout, 0, xt::range(j, j + n)); - d.p = xt::view(eout, 1, xt::range(j, j + n)); + d.e_out = eout.slice(0, tensor::range(j, j + n)); + d.p = eout.slice(1, tensor::range(j, j + n)); // To get answers that match ACE data, for now we still use the tabulated // CDF values that were passed through to the HDF5 library. At a later // time, we can remove the CDF values from the HDF5 library and // reconstruct them using the PDF if (true) { - d.c = xt::view(eout, 2, xt::range(j, j + n)); + d.c = eout.slice(2, tensor::range(j, j + n)); } else { // Calculate cumulative distribution function -- discrete portion for (int k = 0; k < d.n_discrete; ++k) { diff --git a/src/eigenvalue.cpp b/src/eigenvalue.cpp index dc4fbb420b..bc8dcc9ea7 100644 --- a/src/eigenvalue.cpp +++ b/src/eigenvalue.cpp @@ -1,9 +1,6 @@ #include "openmc/eigenvalue.h" -#include "xtensor/xbuilder.hpp" -#include "xtensor/xmath.hpp" -#include "xtensor/xtensor.hpp" -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include "openmc/array.h" #include "openmc/bank.h" @@ -39,7 +36,7 @@ namespace simulation { double keff_generation; array k_sum; vector entropy; -xt::xtensor source_frac; +tensor::Tensor source_frac; } // namespace simulation @@ -452,7 +449,7 @@ int openmc_get_keff(double* k_combined) const auto& gt = simulation::global_tallies; array kv {}; - xt::xtensor cov = xt::zeros({3, 3}); + tensor::Tensor cov = tensor::zeros({3, 3}); kv[0] = gt(GlobalTally::K_COLLISION, TallyResult::SUM) / n; kv[1] = gt(GlobalTally::K_ABSORPTION, TallyResult::SUM) / n; kv[2] = gt(GlobalTally::K_TRACKLENGTH, TallyResult::SUM) / n; @@ -591,7 +588,7 @@ void shannon_entropy() { // Get source weight in each mesh bin bool sites_outside; - xt::xtensor p = + tensor::Tensor p = simulation::entropy_mesh->count_sites(simulation::fission_bank.data(), simulation::fission_bank.size(), &sites_outside); @@ -603,7 +600,7 @@ void shannon_entropy() if (mpi::master) { // Normalize to total weight of bank sites - p /= xt::sum(p); + p /= p.sum(); // Sum values to obtain Shannon entropy double H = 0.0; @@ -627,7 +624,7 @@ void ufs_count_sites() std::size_t n = simulation::ufs_mesh->n_bins(); double vol_frac = simulation::ufs_mesh->volume_frac_; - simulation::source_frac = xt::xtensor({n}, vol_frac); + simulation::source_frac = tensor::Tensor({n}, vol_frac); } else { // count number of source sites in each ufs mesh cell @@ -649,7 +646,7 @@ void ufs_count_sites() #endif // Normalize to total weight to get fraction of source in each cell - double total = xt::sum(simulation::source_frac)(); + double total = simulation::source_frac.sum(); simulation::source_frac /= total; // Since the total starting weight is not equal to n_particles, we need to diff --git a/src/endf.cpp b/src/endf.cpp index c0c1d2e7e8..b298ebe01f 100644 --- a/src/endf.cpp +++ b/src/endf.cpp @@ -5,8 +5,7 @@ #include // for back_inserter #include // for runtime_error -#include "xtensor/xarray.hpp" -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include "openmc/array.h" #include "openmc/constants.h" @@ -153,11 +152,11 @@ Tabulated1D::Tabulated1D(hid_t dset) for (const auto i : int_temp) int_.push_back(int2interp(i)); - xt::xarray arr; + tensor::Tensor arr; read_dataset(dset, arr); - auto xs = xt::view(arr, 0); - auto ys = xt::view(arr, 1); + tensor::View xs = arr.slice(0); + tensor::View ys = arr.slice(1); std::copy(xs.begin(), xs.end(), std::back_inserter(x_)); std::copy(ys.begin(), ys.end(), std::back_inserter(y_)); @@ -229,12 +228,12 @@ double Tabulated1D::operator()(double x) const CoherentElasticXS::CoherentElasticXS(hid_t dset) { // Read 2D array from dataset - xt::xarray arr; + tensor::Tensor arr; read_dataset(dset, arr); // Get views for Bragg edges and structure factors - auto E = xt::view(arr, 0); - auto s = xt::view(arr, 1); + tensor::View E = arr.slice(0); + tensor::View s = arr.slice(1); // Copy Bragg edges and partial sums of structure factors std::copy(E.begin(), E.end(), std::back_inserter(bragg_edges_)); diff --git a/src/finalize.cpp b/src/finalize.cpp index 5ca9d57462..4ac6d09f32 100644 --- a/src/finalize.cpp +++ b/src/finalize.cpp @@ -30,7 +30,7 @@ #include "openmc/volume_calc.h" #include "openmc/weight_windows.h" -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" namespace openmc { @@ -203,7 +203,7 @@ int openmc_reset() // Reset global tallies simulation::n_realizations = 0; - xt::view(simulation::global_tallies, xt::all()) = 0.0; + simulation::global_tallies.fill(0.0); simulation::k_col_abs = 0.0; simulation::k_col_tra = 0.0; diff --git a/src/hdf5_interface.cpp b/src/hdf5_interface.cpp index c56d485e28..00c6a4399c 100644 --- a/src/hdf5_interface.cpp +++ b/src/hdf5_interface.cpp @@ -4,8 +4,7 @@ #include #include -#include "xtensor/xarray.hpp" -#include "xtensor/xtensor.hpp" +#include "openmc/tensor.h" #include #include "hdf5.h" @@ -466,22 +465,19 @@ void read_dataset_lowlevel(hid_t obj_id, const char* name, hid_t mem_type_id, } template<> -void read_dataset(hid_t dset, xt::xarray>& arr, bool indep) +void read_dataset( + hid_t dset, tensor::Tensor>& tensor, bool indep) { // Get shape of dataset vector shape = object_shape(dset); - // Allocate new array to read data into - std::size_t size = 1; - for (const auto x : shape) - size *= x; - vector> buffer(size); + // Resize tensor and read data directly + vector tshape(shape.begin(), shape.end()); + tensor.resize(tshape); - // Read data from attribute - read_complex(dset, nullptr, buffer.data(), indep); - - // Adapt into xarray - arr = xt::adapt(buffer, shape); + // Read data from dataset + read_complex(dset, nullptr, + reinterpret_cast*>(tensor.data()), indep); } void read_double(hid_t obj_id, const char* name, double* buffer, bool indep) diff --git a/src/material.cpp b/src/material.cpp index 072e6decad..21b11b8b9c 100644 --- a/src/material.cpp +++ b/src/material.cpp @@ -8,9 +8,7 @@ #include #include -#include "xtensor/xbuilder.hpp" -#include "xtensor/xoperation.hpp" -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include "openmc/capi.h" #include "openmc/container_util.h" @@ -216,7 +214,7 @@ Material::Material(pugi::xml_node node) // allocate arrays in Material object auto n = names.size(); nuclide_.reserve(n); - atom_density_ = xt::empty({n}); + atom_density_ = tensor::Tensor({n}); if (settings::photon_transport) element_.reserve(n); @@ -290,14 +288,14 @@ Material::Material(pugi::xml_node node) // Check to make sure either all atom percents or all weight percents are // given - if (!(xt::all(atom_density_ >= 0.0) || xt::all(atom_density_ <= 0.0))) { + if (!((atom_density_ >= 0.0).all() || (atom_density_ <= 0.0).all())) { fatal_error( "Cannot mix atom and weight percents in material " + std::to_string(id_)); } // Determine density if it is a sum value if (sum_density) - density_ = xt::sum(atom_density_)(); + density_ = atom_density_.sum(); if (check_for_node(node, "temperature")) { temperature_ = std::stod(get_node_value(node, "temperature")); @@ -435,7 +433,7 @@ void Material::normalize_density() // determine normalized atom percents. if given atom percents, this is // straightforward. if given weight percents, the value is w/awr and is // divided by sum(w/awr) - atom_density_ /= xt::sum(atom_density_)(); + atom_density_ /= atom_density_.sum(); // Change density in g/cm^3 to atom/b-cm. Since all values are now in // atom percent, the sum needs to be re-evaluated as 1/sum(x*awr) @@ -641,14 +639,14 @@ void Material::init_bremsstrahlung() bool positron = (particle == 1); // Allocate arrays for TTB data - ttb->pdf = xt::zeros({n_e, n_e}); - ttb->cdf = xt::zeros({n_e, n_e}); - ttb->yield = xt::zeros({n_e}); + ttb->pdf = tensor::zeros({n_e, n_e}); + ttb->cdf = tensor::zeros({n_e, n_e}); + ttb->yield = tensor::zeros({n_e}); // Allocate temporary arrays - xt::xtensor stopping_power_collision({n_e}, 0.0); - xt::xtensor stopping_power_radiative({n_e}, 0.0); - xt::xtensor dcs({n_e, n_k}, 0.0); + auto stopping_power_collision = tensor::zeros({n_e}); + auto stopping_power_radiative = tensor::zeros({n_e}); + auto dcs = tensor::zeros({n_e, n_k}); double Z_eq_sq = 0.0; double sum_density = 0.0; @@ -698,18 +696,18 @@ void Material::init_bremsstrahlung() 1.0595e-3 * std::pow(t, 5) + 7.0568e-5 * std::pow(t, 6) - 1.808e-6 * std::pow(t, 7)); stopping_power_radiative(i) *= r; - auto dcs_i = xt::view(dcs, i, xt::all()); + tensor::View dcs_i = dcs.slice(i); dcs_i *= r; } } // Total material stopping power - xt::xtensor stopping_power = + tensor::Tensor stopping_power = stopping_power_collision + stopping_power_radiative; // Loop over photon energies - xt::xtensor f({n_e}, 0.0); - xt::xtensor z({n_e}, 0.0); + auto f = tensor::zeros({n_e}); + auto z = tensor::zeros({n_e}); for (int i = 0; i < n_e - 1; ++i) { double w = data::ttb_e_grid(i); @@ -797,7 +795,8 @@ void Material::init_bremsstrahlung() } // Use logarithm of number yield since it is log-log interpolated - ttb->yield = xt::where(ttb->yield > 0.0, xt::log(ttb->yield), -500.0); + ttb->yield = + tensor::where(ttb->yield > 0.0, tensor::log(ttb->yield), -500.0); } } @@ -979,7 +978,7 @@ void Material::set_density(double density, const std::string& units) density_ = density; // Determine normalized atom percents - double sum_percent = xt::sum(atom_density_)(); + double sum_percent = atom_density_.sum(); atom_density_ /= sum_percent; // Recalculate nuclide atom densities based on given density @@ -1020,7 +1019,7 @@ void Material::set_densities( if (n != nuclide_.size()) { nuclide_.resize(n); - atom_density_ = xt::zeros({n}); + atom_density_ = tensor::zeros({n}); if (settings::photon_transport) element_.resize(n); } @@ -1181,8 +1180,8 @@ void Material::add_nuclide(const std::string& name, double density) auto n = nuclide_.size(); // Create copy of atom_density_ array with one extra entry - xt::xtensor atom_density = xt::zeros({n}); - xt::view(atom_density, xt::range(0, n - 1)) = atom_density_; + tensor::Tensor atom_density = tensor::zeros({n}); + atom_density.slice(tensor::range(0, n - 1)) = atom_density_; atom_density(n - 1) = density; atom_density_ = atom_density; diff --git a/src/mesh.cpp b/src/mesh.cpp index 789113ccc3..5ab7ac3988 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -6,6 +6,7 @@ #define _USE_MATH_DEFINES // to make M_PI declared in Intel and MSVC compilers #include // for ceil #include // for size_t +#include // for accumulate #include #ifdef _MSC_VER @@ -16,13 +17,7 @@ #include "mpi.h" #endif -#include "xtensor/xadapt.hpp" -#include "xtensor/xbuilder.hpp" -#include "xtensor/xeval.hpp" -#include "xtensor/xmath.hpp" -#include "xtensor/xsort.hpp" -#include "xtensor/xtensor.hpp" -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include // for fmt #include "openmc/capi.h" @@ -772,11 +767,9 @@ std::string StructuredMesh::bin_label(int bin) const } } -xt::xtensor StructuredMesh::get_x_shape() const +tensor::Tensor StructuredMesh::get_shape_tensor() const { - // because method is const, shape_ is const as well and can't be adapted - auto tmp_shape = shape_; - return xt::adapt(tmp_shape, {n_dimension_}); + return tensor::Tensor(shape_.data(), static_cast(n_dimension_)); } Position StructuredMesh::sample_element( @@ -961,10 +954,11 @@ void UnstructuredMesh::to_hdf5_inner(hid_t mesh_group) const write_dataset(mesh_group, "length_multiplier", length_multiplier_); // write vertex coordinates - xt::xtensor vertices({static_cast(this->n_vertices()), 3}); + tensor::Tensor vertices( + {static_cast(this->n_vertices()), static_cast(3)}); for (int i = 0; i < this->n_vertices(); i++) { auto v = this->vertex(i); - xt::view(vertices, i, xt::all()) = xt::xarray({v.x, v.y, v.z}); + vertices.slice(i) = {v.x, v.y, v.z}; } write_dataset(mesh_group, "vertices", vertices); @@ -972,8 +966,10 @@ void UnstructuredMesh::to_hdf5_inner(hid_t mesh_group) const // write element types and connectivity vector volumes; - xt::xtensor connectivity({static_cast(this->n_bins()), 8}); - xt::xtensor elem_types({static_cast(this->n_bins()), 1}); + tensor::Tensor connectivity( + {static_cast(this->n_bins()), static_cast(8)}); + tensor::Tensor elem_types( + {static_cast(this->n_bins()), static_cast(1)}); for (int i = 0; i < this->n_bins(); i++) { auto conn = this->connectivity(i); @@ -981,21 +977,18 @@ void UnstructuredMesh::to_hdf5_inner(hid_t mesh_group) const // write linear tet element if (conn.size() == 4) { - xt::view(elem_types, i, xt::all()) = - static_cast(ElementType::LINEAR_TET); - xt::view(connectivity, i, xt::all()) = - xt::xarray({conn[0], conn[1], conn[2], conn[3], -1, -1, -1, -1}); + elem_types.slice(i) = static_cast(ElementType::LINEAR_TET); + connectivity.slice(i) = { + conn[0], conn[1], conn[2], conn[3], -1, -1, -1, -1}; // write linear hex element } else if (conn.size() == 8) { - xt::view(elem_types, i, xt::all()) = - static_cast(ElementType::LINEAR_HEX); - xt::view(connectivity, i, xt::all()) = xt::xarray({conn[0], conn[1], - conn[2], conn[3], conn[4], conn[5], conn[6], conn[7]}); + elem_types.slice(i) = static_cast(ElementType::LINEAR_HEX); + connectivity.slice(i) = { + conn[0], conn[1], conn[2], conn[3], conn[4], conn[5], conn[6], conn[7]}; } else { num_elem_skipped++; - xt::view(elem_types, i, xt::all()) = - static_cast(ElementType::UNSUPPORTED); - xt::view(connectivity, i, xt::all()) = -1; + elem_types.slice(i) = static_cast(ElementType::UNSUPPORTED); + connectivity.slice(i) = -1; } } @@ -1096,7 +1089,7 @@ int StructuredMesh::n_surface_bins() const return 4 * n_dimension_ * n_bins(); } -xt::xtensor StructuredMesh::count_sites( +tensor::Tensor StructuredMesh::count_sites( const SourceSite* bank, int64_t length, bool* outside) const { // Determine shape of array for counts @@ -1104,7 +1097,7 @@ xt::xtensor StructuredMesh::count_sites( vector shape = {m}; // Create array of zeros - xt::xarray cnt {shape, 0.0}; + auto cnt = tensor::zeros(shape); bool outside_ = false; for (int64_t i = 0; i < length; i++) { @@ -1123,31 +1116,25 @@ xt::xtensor StructuredMesh::count_sites( cnt(mesh_bin) += site.wgt; } - // Create copy of count data. Since ownership will be acquired by xtensor, - // std::allocator must be used to avoid Valgrind mismatched free() / delete - // warnings. + // Create reduced count data + auto counts = tensor::zeros(shape); int total = cnt.size(); - double* cnt_reduced = std::allocator {}.allocate(total); #ifdef OPENMC_MPI // collect values from all processors MPI_Reduce( - cnt.data(), cnt_reduced, total, MPI_DOUBLE, MPI_SUM, 0, mpi::intracomm); + cnt.data(), counts.data(), total, MPI_DOUBLE, MPI_SUM, 0, mpi::intracomm); // Check if there were sites outside the mesh for any processor if (outside) { MPI_Reduce(&outside_, outside, 1, MPI_C_BOOL, MPI_LOR, 0, mpi::intracomm); } #else - std::copy(cnt.data(), cnt.data() + total, cnt_reduced); + std::copy(cnt.data(), cnt.data() + total, counts.data()); if (outside) *outside = outside_; #endif - // Adapt reduced values in array back into an xarray - auto arr = xt::adapt(cnt_reduced, total, xt::acquire_ownership(), shape); - xt::xarray counts = arr; - return counts; } @@ -1340,10 +1327,10 @@ void StructuredMesh::surface_bins_crossed( int RegularMesh::set_grid() { - auto shape = xt::adapt(shape_, {n_dimension_}); + tensor::Tensor shape(shape_.data(), static_cast(n_dimension_)); // Check that dimensions are all greater than zero - if (xt::any(shape <= 0)) { + if ((shape <= 0).any()) { set_errmsg("All entries for a regular mesh dimensions " "must be positive."); return OPENMC_E_INVALID_ARGUMENT; @@ -1365,13 +1352,13 @@ int RegularMesh::set_grid() } // Check for negative widths - if (xt::any(width_ < 0.0)) { + if ((width_ < 0.0).any()) { set_errmsg("Cannot have a negative width on a regular mesh."); return OPENMC_E_INVALID_ARGUMENT; } // Set width and upper right coordinate - upper_right_ = xt::eval(lower_left_ + shape * width_); + upper_right_ = lower_left_ + shape * width_; } else if (upper_right_.size() > 0) { @@ -1383,7 +1370,7 @@ int RegularMesh::set_grid() } // Check that upper-right is above lower-left - if (xt::any(upper_right_ < lower_left_)) { + if ((upper_right_ < lower_left_).any()) { set_errmsg( "The upper_right coordinates of a regular mesh must be greater than " "the lower_left coordinates."); @@ -1391,11 +1378,11 @@ int RegularMesh::set_grid() } // Set width - width_ = xt::eval((upper_right_ - lower_left_) / shape); + width_ = (upper_right_ - lower_left_) / shape; } // Set material volumes - volume_frac_ = 1.0 / xt::prod(shape)(); + volume_frac_ = 1.0 / shape.prod(); element_volume_ = 1.0; for (int i = 0; i < n_dimension_; i++) { @@ -1411,7 +1398,7 @@ RegularMesh::RegularMesh(pugi::xml_node node) : StructuredMesh {node} fatal_error("Must specify on a regular mesh."); } - xt::xtensor shape = get_node_xarray(node, "dimension"); + tensor::Tensor shape = get_node_tensor(node, "dimension"); int n = n_dimension_ = shape.size(); if (n != 1 && n != 2 && n != 3) { fatal_error("Mesh must be one, two, or three dimensions."); @@ -1421,7 +1408,7 @@ RegularMesh::RegularMesh(pugi::xml_node node) : StructuredMesh {node} // Check for lower-left coordinates if (check_for_node(node, "lower_left")) { // Read mesh lower-left corner location - lower_left_ = get_node_xarray(node, "lower_left"); + lower_left_ = get_node_tensor(node, "lower_left"); } else { fatal_error("Must specify on a mesh."); } @@ -1432,11 +1419,11 @@ RegularMesh::RegularMesh(pugi::xml_node node) : StructuredMesh {node} fatal_error("Cannot specify both and on a mesh."); } - width_ = get_node_xarray(node, "width"); + width_ = get_node_tensor(node, "width"); } else if (check_for_node(node, "upper_right")) { - upper_right_ = get_node_xarray(node, "upper_right"); + upper_right_ = get_node_tensor(node, "upper_right"); } else { fatal_error("Must specify either or on a mesh."); @@ -1454,7 +1441,7 @@ RegularMesh::RegularMesh(hid_t group) : StructuredMesh {group} fatal_error("Must specify on a regular mesh."); } - xt::xtensor shape; + tensor::Tensor shape; read_dataset(group, "dimension", shape); int n = n_dimension_ = shape.size(); if (n != 1 && n != 2 && n != 3) { @@ -1569,13 +1556,13 @@ std::pair, vector> RegularMesh::plot( void RegularMesh::to_hdf5_inner(hid_t mesh_group) const { - write_dataset(mesh_group, "dimension", get_x_shape()); + write_dataset(mesh_group, "dimension", get_shape_tensor()); write_dataset(mesh_group, "lower_left", lower_left_); write_dataset(mesh_group, "upper_right", upper_right_); write_dataset(mesh_group, "width", width_); } -xt::xtensor RegularMesh::count_sites( +tensor::Tensor RegularMesh::count_sites( const SourceSite* bank, int64_t length, bool* outside) const { // Determine shape of array for counts @@ -1583,7 +1570,7 @@ xt::xtensor RegularMesh::count_sites( vector shape = {m}; // Create array of zeros - xt::xarray cnt {shape, 0.0}; + auto cnt = tensor::zeros(shape); bool outside_ = false; for (int64_t i = 0; i < length; i++) { @@ -1602,31 +1589,25 @@ xt::xtensor RegularMesh::count_sites( cnt(mesh_bin) += site.wgt; } - // Create copy of count data. Since ownership will be acquired by xtensor, - // std::allocator must be used to avoid Valgrind mismatched free() / delete - // warnings. + // Create reduced count data + auto counts = tensor::zeros(shape); int total = cnt.size(); - double* cnt_reduced = std::allocator {}.allocate(total); #ifdef OPENMC_MPI // collect values from all processors MPI_Reduce( - cnt.data(), cnt_reduced, total, MPI_DOUBLE, MPI_SUM, 0, mpi::intracomm); + cnt.data(), counts.data(), total, MPI_DOUBLE, MPI_SUM, 0, mpi::intracomm); // Check if there were sites outside the mesh for any processor if (outside) { MPI_Reduce(&outside_, outside, 1, MPI_C_BOOL, MPI_LOR, 0, mpi::intracomm); } #else - std::copy(cnt.data(), cnt.data() + total, cnt_reduced); + std::copy(cnt.data(), cnt.data() + total, counts.data()); if (outside) *outside = outside_; #endif - // Adapt reduced values in array back into an xarray - auto arr = xt::adapt(cnt_reduced, total, xt::acquire_ownership(), shape); - xt::xarray counts = arr; - return counts; } @@ -2698,7 +2679,7 @@ extern "C" int openmc_regular_mesh_get_params( return err; RegularMesh* m = dynamic_cast(model::meshes[index].get()); - if (m->lower_left_.dimension() == 0) { + if (m->lower_left_.empty()) { set_errmsg("Mesh parameters have not been set."); return OPENMC_E_ALLOCATE; } @@ -2725,17 +2706,17 @@ extern "C" int openmc_regular_mesh_set_params( vector shape = {static_cast(n)}; if (ll && ur) { - m->lower_left_ = xt::adapt(ll, n, xt::no_ownership(), shape); - m->upper_right_ = xt::adapt(ur, n, xt::no_ownership(), shape); - m->width_ = (m->upper_right_ - m->lower_left_) / m->get_x_shape(); + m->lower_left_ = tensor::Tensor(ll, n); + m->upper_right_ = tensor::Tensor(ur, n); + m->width_ = (m->upper_right_ - m->lower_left_) / m->get_shape_tensor(); } else if (ll && width) { - m->lower_left_ = xt::adapt(ll, n, xt::no_ownership(), shape); - m->width_ = xt::adapt(width, n, xt::no_ownership(), shape); - m->upper_right_ = m->lower_left_ + m->get_x_shape() * m->width_; + m->lower_left_ = tensor::Tensor(ll, n); + m->width_ = tensor::Tensor(width, n); + m->upper_right_ = m->lower_left_ + m->get_shape_tensor() * m->width_; } else if (ur && width) { - m->upper_right_ = xt::adapt(ur, n, xt::no_ownership(), shape); - m->width_ = xt::adapt(width, n, xt::no_ownership(), shape); - m->lower_left_ = m->upper_right_ - m->get_x_shape() * m->width_; + m->upper_right_ = tensor::Tensor(ur, n); + m->width_ = tensor::Tensor(width, n); + m->lower_left_ = m->upper_right_ - m->get_shape_tensor() * m->width_; } else { set_errmsg("At least two parameters must be specified."); return OPENMC_E_INVALID_ARGUMENT; @@ -2745,7 +2726,7 @@ extern "C" int openmc_regular_mesh_set_params( // TODO: incorporate this into method in RegularMesh that can be called from // here and from constructor - m->volume_frac_ = 1.0 / xt::prod(m->get_x_shape())(); + m->volume_frac_ = 1.0 / m->get_shape_tensor().prod(); m->element_volume_ = 1.0; for (int i = 0; i < m->n_dimension_; i++) { m->element_volume_ *= m->width_[i]; @@ -2794,7 +2775,7 @@ int openmc_structured_mesh_get_grid_impl(int32_t index, double** grid_x, return err; C* m = dynamic_cast(model::meshes[index].get()); - if (m->lower_left_.dimension() == 0) { + if (m->lower_left_.empty()) { set_errmsg("Mesh parameters have not been set."); return OPENMC_E_ALLOCATE; } diff --git a/src/mgxs.cpp b/src/mgxs.cpp index ea78238b80..65a7a0c55c 100644 --- a/src/mgxs.cpp +++ b/src/mgxs.cpp @@ -5,10 +5,7 @@ #include #include -#include "xtensor/xadapt.hpp" -#include "xtensor/xmath.hpp" -#include "xtensor/xsort.hpp" -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include #include "openmc/error.h" @@ -33,8 +30,7 @@ void Mgxs::init(const std::string& in_name, double in_awr, // Set the metadata name = in_name; awr = in_awr; - // TODO: Remove adapt when in_KTs is an xtensor - kTs = xt::adapt(in_kTs); + kTs = tensor::Tensor(in_kTs.data(), in_kTs.size()); fissionable = in_fissionable; scatter_format = in_scatter_format; xs.resize(in_kTs.size()); @@ -73,7 +69,7 @@ void Mgxs::metadata_from_hdf5(hid_t xs_id, const vector& temperature, } get_datasets(kT_group, dset_names); vector shape = {num_temps}; - xt::xarray temps_available(shape); + tensor::Tensor temps_available(shape); for (int i = 0; i < num_temps; i++) { read_double(kT_group, dset_names[i], &temps_available[i], true); @@ -108,19 +104,7 @@ void Mgxs::metadata_from_hdf5(hid_t xs_id, const vector& temperature, // Determine actual temperatures to read for (const auto& T : temperature) { // Determine the closest temperature value - // NOTE: the below block could be replaced with the following line, - // though this gives a runtime error if using LLVM 20 or newer, - // likely due to a bug in xtensor. - // auto i_closest = xt::argmin(xt::abs(temps_available - T))[0]; - double closest = std::numeric_limits::max(); - int i_closest = 0; - for (int i = 0; i < temps_available.size(); i++) { - double diff = std::abs(temps_available[i] - T); - if (diff < closest) { - closest = diff; - i_closest = i; - } - } + auto i_closest = tensor::abs(temps_available - T).argmin(); double temp_actual = temps_available[i_closest]; if (std::fabs(temp_actual - T) < settings::temperature_tolerance) { @@ -355,7 +339,7 @@ Mgxs::Mgxs(const std::string& in_name, const vector& mat_kTs, for (int m = 0; m < micros.size(); m++) { switch (settings::temperature_method) { case TemperatureMethod::NEAREST: { - micro_t[m] = xt::argmin(xt::abs(micros[m]->kTs - temp_desired))[0]; + micro_t[m] = tensor::abs(micros[m]->kTs - temp_desired).argmin(); auto temp_actual = micros[m]->kTs[micro_t[m]]; if (std::abs(temp_actual - temp_desired) >= @@ -368,7 +352,7 @@ Mgxs::Mgxs(const std::string& in_name, const vector& mat_kTs, case TemperatureMethod::INTERPOLATION: // Get a list of bounding temperatures for each actual temperature // present in the model - for (int k = 0; k < micros[m]->kTs.shape()[0] - 1; k++) { + for (int k = 0; k < micros[m]->kTs.shape(0) - 1; k++) { if ((micros[m]->kTs[k] <= temp_desired) && (temp_desired < micros[m]->kTs[k + 1])) { micro_t[m] = k; @@ -474,7 +458,7 @@ double Mgxs::get_xs(MgxsType xstype, int gin, const int* gout, const double* mu, val = xs_t->delayed_nu_fission(a, *dg, gin); } else { val = 0.; - for (int d = 0; d < xs_t->delayed_nu_fission.shape()[1]; d++) { + for (int d = 0; d < xs_t->delayed_nu_fission.shape(1); d++) { val += xs_t->delayed_nu_fission(a, d, gin); } } @@ -489,7 +473,7 @@ double Mgxs::get_xs(MgxsType xstype, int gin, const int* gout, const double* mu, } else { // provide an outgoing group-wise sum val = 0.; - for (int g = 0; g < xs_t->chi_prompt.shape()[2]; g++) { + for (int g = 0; g < xs_t->chi_prompt.shape(2); g++) { val += xs_t->chi_prompt(a, gin, g); } } @@ -508,13 +492,13 @@ double Mgxs::get_xs(MgxsType xstype, int gin, const int* gout, const double* mu, } else { if (dg != nullptr) { val = 0.; - for (int g = 0; g < xs_t->delayed_nu_fission.shape()[2]; g++) { + for (int g = 0; g < xs_t->delayed_nu_fission.shape(2); g++) { val += xs_t->delayed_nu_fission(a, *dg, gin, g); } } else { val = 0.; - for (int g = 0; g < xs_t->delayed_nu_fission.shape()[2]; g++) { - for (int d = 0; d < xs_t->delayed_nu_fission.shape()[3]; d++) { + for (int g = 0; g < xs_t->delayed_nu_fission.shape(2); g++) { + for (int d = 0; d < xs_t->delayed_nu_fission.shape(3); d++) { val += xs_t->delayed_nu_fission(a, d, gin, g); } } @@ -650,7 +634,7 @@ bool Mgxs::equiv(const Mgxs& that) int Mgxs::get_temperature_index(double sqrtkT) const { - return xt::argmin(xt::abs(kTs - sqrtkT * sqrtkT))[0]; + return tensor::abs(kTs - sqrtkT * sqrtkT).argmin(); } //============================================================================== diff --git a/src/nuclide.cpp b/src/nuclide.cpp index 69e603a7c6..17d6e952c3 100644 --- a/src/nuclide.cpp +++ b/src/nuclide.cpp @@ -17,8 +17,7 @@ #include -#include "xtensor/xbuilder.hpp" -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include // for sort, min_element #include @@ -361,8 +360,7 @@ void Nuclide::create_derived( { for (const auto& grid : grid_) { // Allocate and initialize cross section - array shape {grid.energy.size(), 5}; - xs_.emplace_back(shape, 0.0); + xs_.push_back(tensor::zeros({grid.energy.size(), 5})); } reaction_index_.fill(C_NONE); @@ -375,9 +373,8 @@ void Nuclide::create_derived( for (int t = 0; t < kTs_.size(); ++t) { int j = rx->xs_[t].threshold; int n = rx->xs_[t].value.size(); - auto xs = xt::adapt(rx->xs_[t].value); - auto pprod = xt::view(xs_[t], xt::range(j, j + n), XS_PHOTON_PROD); - + auto xs = tensor::Tensor( + rx->xs_[t].value.data(), rx->xs_[t].value.size()); for (const auto& p : rx->products_) { if (p.particle_.is_photon()) { for (int k = 0; k < n; ++k) { @@ -396,7 +393,7 @@ void Nuclide::create_derived( } } - pprod[k] += f * xs[k] * (*p.yield_)(E); + xs_[t](j + k, XS_PHOTON_PROD) += f * xs[k] * (*p.yield_)(E); } } } @@ -406,20 +403,17 @@ void Nuclide::create_derived( continue; // Add contribution to total cross section - auto total = xt::view(xs_[t], xt::range(j, j + n), XS_TOTAL); - total += xs; + xs_[t].slice(tensor::range(j, j + n), XS_TOTAL) += xs; // Add contribution to absorption cross section - auto absorption = xt::view(xs_[t], xt::range(j, j + n), XS_ABSORPTION); if (is_disappearance(rx->mt_)) { - absorption += xs; + xs_[t].slice(tensor::range(j, j + n), XS_ABSORPTION) += xs; } if (is_fission(rx->mt_)) { fissionable_ = true; - auto fission = xt::view(xs_[t], xt::range(j, j + n), XS_FISSION); - fission += xs; - absorption += xs; + xs_[t].slice(tensor::range(j, j + n), XS_FISSION) += xs; + xs_[t].slice(tensor::range(j, j + n), XS_ABSORPTION) += xs; // Keep track of fission reactions if (t == 0) { @@ -510,7 +504,7 @@ void Nuclide::init_grid() double spacing = std::log(E_max / E_min) / M; // Create equally log-spaced energy grid - auto umesh = xt::linspace(0.0, M * spacing, M + 1); + auto umesh = tensor::linspace(0.0, M * spacing, M + 1); for (auto& grid : grid_) { // Resize array for storing grid indices diff --git a/src/output.cpp b/src/output.cpp index ae2daaffc1..8f9802bb9b 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -17,7 +17,7 @@ #ifdef _OPENMP #include #endif -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include "openmc/capi.h" #include "openmc/cell.h" diff --git a/src/photon.cpp b/src/photon.cpp index 951acb9fbd..5e2ba68842 100644 --- a/src/photon.cpp +++ b/src/photon.cpp @@ -13,11 +13,7 @@ #include "openmc/search.h" #include "openmc/settings.h" -#include "xtensor/xbuilder.hpp" -#include "xtensor/xmath.hpp" -#include "xtensor/xoperation.hpp" -#include "xtensor/xslice.hpp" -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include #include @@ -33,7 +29,7 @@ constexpr int PhotonInteraction::MAX_STACK_SIZE; namespace data { -xt::xtensor compton_profile_pz; +tensor::Tensor compton_profile_pz; std::unordered_map element_map; vector> elements; @@ -46,8 +42,6 @@ vector> elements; PhotonInteraction::PhotonInteraction(hid_t group) { - using namespace xt::placeholders; - // Set index of element in global vector index_ = data::elements.size(); @@ -96,7 +90,7 @@ PhotonInteraction::PhotonInteraction(hid_t group) read_dataset(rgroup, "xs", pair_production_electron_); close_group(rgroup); } else { - pair_production_electron_ = xt::zeros_like(energy_); + pair_production_electron_ = tensor::zeros_like(energy_); } // Read pair production @@ -105,7 +99,7 @@ PhotonInteraction::PhotonInteraction(hid_t group) read_dataset(rgroup, "xs", pair_production_nuclear_); close_group(rgroup); } else { - pair_production_nuclear_ = xt::zeros_like(energy_); + pair_production_nuclear_ = tensor::zeros_like(energy_); } // Read photoelectric @@ -119,7 +113,7 @@ PhotonInteraction::PhotonInteraction(hid_t group) read_dataset(rgroup, "xs", heating_); close_group(rgroup); } else { - heating_ = xt::zeros_like(energy_); + heating_ = tensor::zeros_like(energy_); } // Read subshell photoionization cross section and atomic relaxation data @@ -133,7 +127,7 @@ PhotonInteraction::PhotonInteraction(hid_t group) } shells_.resize(n_shell); - cross_sections_ = xt::zeros({energy_.size(), n_shell}); + cross_sections_ = tensor::zeros({energy_.size(), n_shell}); // Create mapping from designator to index std::unordered_map shell_map; @@ -168,15 +162,17 @@ PhotonInteraction::PhotonInteraction(hid_t group) } // Read subshell cross section - xt::xtensor xs; + tensor::Tensor xs; dset = open_dataset(tgroup, "xs"); read_attribute(dset, "threshold_idx", shell.threshold); close_dataset(dset); read_dataset(tgroup, "xs", xs); auto cross_section = - xt::view(cross_sections_, xt::range(shell.threshold, _), i); - cross_section = xt::where(xs > 0, xt::log(xs), 0); + cross_sections_.slice(tensor::range(static_cast(shell.threshold), + cross_sections_.shape(0)), + i); + cross_section = tensor::where(xs > 0, tensor::log(xs), 0); if (object_exists(tgroup, "transitions")) { // Determine dimensions of transitions @@ -186,11 +182,12 @@ PhotonInteraction::PhotonInteraction(hid_t group) int n_transition = dims[0]; if (n_transition > 0) { - xt::xtensor matrix; + tensor::Tensor matrix; read_dataset(tgroup, "transitions", matrix); // Transition probability normalization - double norm = xt::sum(xt::col(matrix, 3))(); + double norm = + tensor::Tensor(matrix.slice(tensor::all, 3)).sum(); shell.transitions.resize(n_transition); for (int j = 0; j < n_transition; ++j) { @@ -220,7 +217,7 @@ PhotonInteraction::PhotonInteraction(hid_t group) // Read electron shell PDF and binding energies read_dataset(rgroup, "num_electrons", electron_pdf_); - electron_pdf_ /= xt::sum(electron_pdf_); + electron_pdf_ /= electron_pdf_.sum(); read_dataset(rgroup, "binding_energy", binding_energy_); // Read Compton profiles @@ -238,7 +235,7 @@ PhotonInteraction::PhotonInteraction(hid_t group) auto is_close = [](double a, double b) { return std::abs(a - b) / a < FP_REL_PRECISION; }; - subshell_map_ = xt::full_like(binding_energy_, -1); + subshell_map_ = tensor::Tensor(binding_energy_.shape(), -1); for (int i = 0; i < binding_energy_.size(); ++i) { double E_b = binding_energy_[i]; if (i < n_shell && is_close(E_b, shells_[i].binding_energy)) { @@ -257,7 +254,7 @@ PhotonInteraction::PhotonInteraction(hid_t group) // Create Compton profile CDF auto n_profile = data::compton_profile_pz.size(); auto n_shell_compton = profile_pdf_.shape(0); - profile_cdf_ = xt::empty({n_shell_compton, n_profile}); + profile_cdf_ = tensor::Tensor({n_shell_compton, n_profile}); for (int i = 0; i < n_shell_compton; ++i) { double c = 0.0; profile_cdf_(i, 0) = 0.0; @@ -276,11 +273,11 @@ PhotonInteraction::PhotonInteraction(hid_t group) // Read bremsstrahlung scaled DCS rgroup = open_group(group, "bremsstrahlung"); read_dataset(rgroup, "dcs", dcs_); - auto n_e = dcs_.shape()[0]; - auto n_k = dcs_.shape()[1]; + auto n_e = dcs_.shape(0); + auto n_k = dcs_.shape(1); // Get energy grids used for bremsstrahlung DCS and for stopping powers - xt::xtensor electron_energy; + tensor::Tensor electron_energy; read_dataset(rgroup, "electron_energy", electron_energy); if (data::ttb_k_grid.size() == 0) { read_dataset(rgroup, "photon_energy", data::ttb_k_grid); @@ -305,12 +302,12 @@ PhotonInteraction::PhotonInteraction(hid_t group) (std::log(E(i_grid + 1)) - std::log(E(i_grid))); // Interpolate bremsstrahlung DCS at the cutoff energy and truncate - xt::xtensor dcs({n_e - i_grid, n_k}); + tensor::Tensor dcs({n_e - i_grid, n_k}); for (int i = 0; i < n_k; ++i) { double y = std::exp( std::log(dcs_(i_grid, i)) + f * (std::log(dcs_(i_grid + 1, i)) - std::log(dcs_(i_grid, i)))); - auto col_i = xt::view(dcs, xt::all(), i); + tensor::View col_i = dcs.slice(tensor::all, i); col_i(0) = y; for (int j = i_grid + 1; j < n_e; ++j) { col_i(j - i_grid) = dcs_(j, i); @@ -318,9 +315,11 @@ PhotonInteraction::PhotonInteraction(hid_t group) } dcs_ = dcs; - xt::xtensor frst {cutoff}; - electron_energy = xt::concatenate(xt::xtuple( - frst, xt::view(electron_energy, xt::range(i_grid + 1, n_e)))); + tensor::Tensor frst({static_cast(1)}); + frst(0) = cutoff; + tensor::Tensor rest(electron_energy.slice( + tensor::range(i_grid + 1, electron_energy.size()))); + electron_energy = tensor::concatenate(frst, rest); } // Set incident particle energy grid @@ -329,7 +328,8 @@ PhotonInteraction::PhotonInteraction(hid_t group) } // Calculate the radiative stopping power - stopping_power_radiative_ = xt::empty({data::ttb_e_grid.size()}); + stopping_power_radiative_ = + tensor::Tensor({data::ttb_e_grid.size()}); for (int i = 0; i < data::ttb_e_grid.size(); ++i) { // Integrate over reduced photon energy double c = 0.0; @@ -354,14 +354,15 @@ PhotonInteraction::PhotonInteraction(hid_t group) // values below exp(-499) we store the log as -900, for which exp(-900) // evaluates to zero. double limit = std::exp(-499.0); - energy_ = xt::log(energy_); - coherent_ = xt::where(coherent_ > limit, xt::log(coherent_), -900.0); - incoherent_ = xt::where(incoherent_ > limit, xt::log(incoherent_), -900.0); - photoelectric_total_ = xt::where( - photoelectric_total_ > limit, xt::log(photoelectric_total_), -900.0); - pair_production_total_ = xt::where( - pair_production_total_ > limit, xt::log(pair_production_total_), -900.0); - heating_ = xt::where(heating_ > limit, xt::log(heating_), -900.0); + energy_ = tensor::log(energy_); + coherent_ = tensor::where(coherent_ > limit, tensor::log(coherent_), -900.0); + incoherent_ = + tensor::where(incoherent_ > limit, tensor::log(incoherent_), -900.0); + photoelectric_total_ = tensor::where( + photoelectric_total_ > limit, tensor::log(photoelectric_total_), -900.0); + pair_production_total_ = tensor::where(pair_production_total_ > limit, + tensor::log(pair_production_total_), -900.0); + heating_ = tensor::where(heating_ > limit, tensor::log(heating_), -900.0); } PhotonInteraction::~PhotonInteraction() @@ -512,7 +513,7 @@ void PhotonInteraction::compton_doppler( c = prn(seed) * c_max; // Determine pz corresponding to sampled cdf value - auto cdf_shell = xt::view(profile_cdf_, shell, xt::all()); + tensor::View cdf_shell = profile_cdf_.slice(shell); int i = lower_bound_index(cdf_shell.cbegin(), cdf_shell.cend(), c); double pz_l = data::compton_profile_pz(i); double pz_r = data::compton_profile_pz(i + 1); @@ -608,8 +609,8 @@ void PhotonInteraction::calculate_xs(Particle& p) const // Calculate microscopic photoelectric cross section xs.photoelectric = 0.0; - const auto& xs_lower = xt::row(cross_sections_, i_grid); - const auto& xs_upper = xt::row(cross_sections_, i_grid + 1); + tensor::View xs_lower = cross_sections_.slice(i_grid); + tensor::View xs_upper = cross_sections_.slice(i_grid + 1); for (int i = 0; i < xs_upper.size(); ++i) if (xs_lower(i) != 0) diff --git a/src/physics.cpp b/src/physics.cpp index 1f72e4fac0..c9737205ce 100644 --- a/src/physics.cpp +++ b/src/physics.cpp @@ -30,9 +30,9 @@ #include +#include "openmc/tensor.h" #include // for max, min, max_element #include // for sqrt, exp, log, abs, copysign -#include namespace openmc { @@ -375,8 +375,9 @@ void sample_photon_reaction(Particle& p) // cross sections int i_grid = micro.index_grid; double f = micro.interp_factor; - const auto& xs_lower = xt::row(element.cross_sections_, i_grid); - const auto& xs_upper = xt::row(element.cross_sections_, i_grid + 1); + tensor::View xs_lower = element.cross_sections_.slice(i_grid); + tensor::View xs_upper = + element.cross_sections_.slice(i_grid + 1); for (int i_shell = 0; i_shell < element.shells_.size(); ++i_shell) { const auto& shell {element.shells_[i_shell]}; diff --git a/src/physics_mg.cpp b/src/physics_mg.cpp index 58a21a6789..32ae10a609 100644 --- a/src/physics_mg.cpp +++ b/src/physics_mg.cpp @@ -2,7 +2,7 @@ #include -#include "xtensor/xarray.hpp" +#include "openmc/tensor.h" #include #include "openmc/bank.h" diff --git a/src/plot.cpp b/src/plot.cpp index e3b1e84c80..ea0555202d 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -7,8 +7,7 @@ #include #include -#include "xtensor/xmanipulation.hpp" -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include #include #ifdef USE_LIBPNG @@ -74,7 +73,8 @@ void IdData::set_value(size_t y, size_t x, const GeometryState& p, int level) void IdData::set_overlap(size_t y, size_t x) { - xt::view(data_, y, x, xt::all()) = OVERLAP; + for (size_t k = 0; k < data_.shape(2); ++k) + data_(y, x, k) = OVERLAP; } PropertyData::PropertyData(size_t h_res, size_t v_res) @@ -783,14 +783,14 @@ void output_ppm(const std::string& filename, const ImageData& data) // Write header of << "P6\n"; - of << data.shape()[0] << " " << data.shape()[1] << "\n"; + of << data.shape(0) << " " << data.shape(1) << "\n"; of << "255\n"; of.close(); of.open(fname, std::ios::binary | std::ios::app); // Write color for each pixel - for (int y = 0; y < data.shape()[1]; y++) { - for (int x = 0; x < data.shape()[0]; x++) { + for (int y = 0; y < data.shape(1); y++) { + for (int x = 0; x < data.shape(0); x++) { RGBColor rgb = data(x, y); of << rgb.red << rgb.green << rgb.blue; } @@ -822,8 +822,8 @@ void output_png(const std::string& filename, const ImageData& data) png_init_io(png_ptr, fp); // Write header (8 bit colour depth) - int width = data.shape()[0]; - int height = data.shape()[1]; + int width = data.shape(0); + int height = data.shape(1); png_set_IHDR(png_ptr, info_ptr, width, height, 8, PNG_COLOR_TYPE_RGB, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE); png_write_info(png_ptr, info_ptr); @@ -1024,9 +1024,14 @@ void Plot::create_voxel() const // select only cell/material ID data and flip the y-axis int idx = color_by_ == PlotColorBy::cells ? 0 : 2; - xt::xtensor data_slice = - xt::view(ids.data_, xt::all(), xt::all(), idx); - xt::xtensor data_flipped = xt::flip(data_slice, 0); + // Extract 2D slice at index idx from 3D data + size_t rows = ids.data_.shape(0); + size_t cols = ids.data_.shape(1); + tensor::Tensor data_slice({rows, cols}); + for (size_t r = 0; r < rows; ++r) + for (size_t c = 0; c < cols; ++c) + data_slice(r, c) = ids.data_(r, c, idx); + tensor::Tensor data_flipped = data_slice.flip(0); // Write to HDF5 dataset voxel_write_slice(z, dspace, dset, memspace, data_flipped.data()); @@ -1272,7 +1277,8 @@ ImageData WireframeRayTracePlot::create_image() const // This array marks where the initial wireframe was drawn. We convolve it with // a filter that gets adjusted with the wireframe thickness in order to // thicken the lines. - xt::xtensor wireframe_initial({width, height}, 0); + tensor::Tensor wireframe_initial( + {static_cast(width), static_cast(height)}, 0); /* Holds all of the track segments for the current rendered line of pixels. * old_segments holds a copy of this_line_segments from the previous line. diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 13a1b0163b..b881ced716 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -18,6 +18,7 @@ #include "openmc/weight_windows.h" #include +#include namespace openmc { @@ -63,8 +64,7 @@ FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_) // Create a new 2D tensor with the same size as the first // two dimensions of the 3D tensor - tally_volumes_[i] = - xt::xtensor::from_shape({shape[0], shape[1]}); + tally_volumes_[i] = tensor::Tensor({shape[0], shape[1]}); } } diff --git a/src/random_ray/random_ray.cpp b/src/random_ray/random_ray.cpp index 47d901d63a..44e161a8e6 100644 --- a/src/random_ray/random_ray.cpp +++ b/src/random_ray/random_ray.cpp @@ -10,6 +10,8 @@ #include "openmc/settings.h" #include "openmc/simulation.h" +#include + #include "openmc/distribution_spatial.h" #include "openmc/random_dist.h" #include "openmc/source.h" diff --git a/src/scattdata.cpp b/src/scattdata.cpp index 21b18cbd92..9aa09956d5 100644 --- a/src/scattdata.cpp +++ b/src/scattdata.cpp @@ -4,8 +4,7 @@ #include #include -#include "xtensor/xbuilder.hpp" -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include "openmc/constants.h" #include "openmc/error.h" @@ -19,8 +18,8 @@ namespace openmc { // ScattData base-class methods //============================================================================== -void ScattData::base_init(int order, const xt::xtensor& in_gmin, - const xt::xtensor& in_gmax, const double_2dvec& in_energy, +void ScattData::base_init(int order, const tensor::Tensor& in_gmin, + const tensor::Tensor& in_gmax, const double_2dvec& in_energy, const double_2dvec& in_mult) { size_t groups = in_energy.size(); @@ -63,23 +62,26 @@ void ScattData::base_init(int order, const xt::xtensor& in_gmin, void ScattData::base_combine(size_t max_order, size_t order_dim, const vector& those_scatts, const vector& scalars, - xt::xtensor& in_gmin, xt::xtensor& in_gmax, + tensor::Tensor& in_gmin, tensor::Tensor& in_gmax, double_2dvec& sparse_mult, double_3dvec& sparse_scatter) { size_t groups = those_scatts[0]->energy.size(); // Now allocate and zero our storage spaces - xt::xtensor this_nuscatt_matrix({groups, groups, order_dim}, 0.); - xt::xtensor this_nuscatt_P0({groups, groups}, 0.); - xt::xtensor this_scatt_P0({groups, groups}, 0.); - xt::xtensor this_mult({groups, groups}, 1.); + tensor::Tensor this_nuscatt_matrix = + tensor::zeros({groups, groups, order_dim}); + tensor::Tensor this_nuscatt_P0 = + tensor::zeros({groups, groups}); + tensor::Tensor this_scatt_P0 = + tensor::zeros({groups, groups}); + tensor::Tensor this_mult = tensor::ones({groups, groups}); // Build the dense scattering and multiplicity matrices for (int i = 0; i < those_scatts.size(); i++) { ScattData* that = those_scatts[i]; // Build the dense matrix for that object - xt::xtensor that_matrix = that->get_matrix(max_order); + tensor::Tensor that_matrix = that->get_matrix(max_order); // Now add that to this for the nu-scatter matrix this_nuscatt_matrix += scalars[i] * that_matrix; @@ -97,7 +99,7 @@ void ScattData::base_combine(size_t max_order, size_t order_dim, // Now we have the dense nuscatt and scatt, we can easily compute the // multiplicity matrix by dividing the two and fixing any nans - this_mult = xt::nan_to_num(this_nuscatt_P0 / this_scatt_P0); + this_mult = tensor::nan_to_num(this_nuscatt_P0 / this_scatt_P0); // We have the data, now we need to convert to a jagged array and then use // the initialize function to store it on the object. @@ -106,7 +108,7 @@ void ScattData::base_combine(size_t max_order, size_t order_dim, int gmin_; for (gmin_ = 0; gmin_ < groups; gmin_++) { bool non_zero = false; - for (int l = 0; l < this_nuscatt_matrix.shape()[2]; l++) { + for (int l = 0; l < this_nuscatt_matrix.shape(2); l++) { if (this_nuscatt_matrix(gin, gmin_, l) != 0.) { non_zero = true; break; @@ -118,7 +120,7 @@ void ScattData::base_combine(size_t max_order, size_t order_dim, int gmax_; for (gmax_ = groups - 1; gmax_ >= 0; gmax_--) { bool non_zero = false; - for (int l = 0; l < this_nuscatt_matrix.shape()[2]; l++) { + for (int l = 0; l < this_nuscatt_matrix.shape(2); l++) { if (this_nuscatt_matrix(gin, gmax_, l) != 0.) { non_zero = true; break; @@ -143,8 +145,8 @@ void ScattData::base_combine(size_t max_order, size_t order_dim, sparse_mult[gin].resize(gmax_ - gmin_ + 1); int i_gout = 0; for (int gout = gmin_; gout <= gmax_; gout++) { - sparse_scatter[gin][i_gout].resize(this_nuscatt_matrix.shape()[2]); - for (int l = 0; l < this_nuscatt_matrix.shape()[2]; l++) { + sparse_scatter[gin][i_gout].resize(this_nuscatt_matrix.shape(2)); + for (int l = 0; l < this_nuscatt_matrix.shape(2); l++) { sparse_scatter[gin][i_gout][l] = this_nuscatt_matrix(gin, gout, l); } sparse_mult[gin][i_gout] = this_mult(gin, gout); @@ -227,8 +229,8 @@ double ScattData::get_xs( // ScattDataLegendre methods //============================================================================== -void ScattDataLegendre::init(const xt::xtensor& in_gmin, - const xt::xtensor& in_gmax, const double_2dvec& in_mult, +void ScattDataLegendre::init(const tensor::Tensor& in_gmin, + const tensor::Tensor& in_gmax, const double_2dvec& in_mult, const double_3dvec& coeffs) { size_t groups = coeffs.size(); @@ -239,7 +241,7 @@ void ScattDataLegendre::init(const xt::xtensor& in_gmin, // Get the scattering cross section value by summing the un-normalized P0 // coefficient in the variable matrix over all outgoing groups. - scattxs = xt::zeros({groups}); + scattxs = tensor::zeros({groups}); for (int gin = 0; gin < groups; gin++) { int num_groups = in_gmax[gin] - in_gmin[gin] + 1; for (int i_gout = 0; i_gout < num_groups; i_gout++) { @@ -386,8 +388,8 @@ void ScattDataLegendre::combine( size_t groups = those_scatts[0]->energy.size(); - xt::xtensor in_gmin({groups}, 0); - xt::xtensor in_gmax({groups}, 0); + tensor::Tensor in_gmin({groups}, 0); + tensor::Tensor in_gmax({groups}, 0); double_3dvec sparse_scatter(groups); double_2dvec sparse_mult(groups); @@ -404,12 +406,13 @@ void ScattDataLegendre::combine( //============================================================================== -xt::xtensor ScattDataLegendre::get_matrix(size_t max_order) +tensor::Tensor ScattDataLegendre::get_matrix(size_t max_order) { // Get the sizes and initialize the data to 0 size_t groups = energy.size(); size_t order_dim = max_order + 1; - xt::xtensor matrix({groups, groups, order_dim}, 0.); + tensor::Tensor matrix = + tensor::zeros({groups, groups, order_dim}); for (int gin = 0; gin < groups; gin++) { for (int i_gout = 0; i_gout < energy[gin].size(); i_gout++) { @@ -427,8 +430,8 @@ xt::xtensor ScattDataLegendre::get_matrix(size_t max_order) // ScattDataHistogram methods //============================================================================== -void ScattDataHistogram::init(const xt::xtensor& in_gmin, - const xt::xtensor& in_gmax, const double_2dvec& in_mult, +void ScattDataHistogram::init(const tensor::Tensor& in_gmin, + const tensor::Tensor& in_gmax, const double_2dvec& in_mult, const double_3dvec& coeffs) { size_t groups = coeffs.size(); @@ -439,7 +442,7 @@ void ScattDataHistogram::init(const xt::xtensor& in_gmin, // Get the scattering cross section value by summing the distribution // over all the histogram bins in angle and outgoing energy groups - scattxs = xt::zeros({groups}); + scattxs = tensor::zeros({groups}); for (int gin = 0; gin < groups; gin++) { for (int i_gout = 0; i_gout < matrix[gin].size(); i_gout++) { scattxs[gin] += std::accumulate( @@ -468,7 +471,7 @@ void ScattDataHistogram::init(const xt::xtensor& in_gmin, ScattData::base_init(order, in_gmin, in_gmax, in_energy, in_mult); // Build the angular distribution mu values - mu = xt::linspace(-1., 1., order + 1); + mu = tensor::linspace(-1., 1., order + 1); dmu = 2. / order; // Calculate f(mu) and integrate it so we can avoid rejection sampling @@ -513,7 +516,7 @@ double ScattDataHistogram::calc_f(int gin, int gout, double mu) int imu; if (mu == 1.) { // use size -2 to have the index one before the end - imu = this->mu.shape()[0] - 2; + imu = this->mu.shape(0) - 2; } else { imu = std::floor((mu + 1.) / dmu + 1.) - 1; } @@ -559,13 +562,13 @@ void ScattDataHistogram::sample( //============================================================================== -xt::xtensor ScattDataHistogram::get_matrix(size_t max_order) +tensor::Tensor ScattDataHistogram::get_matrix(size_t max_order) { // Get the sizes and initialize the data to 0 size_t groups = energy.size(); // We ignore the requested order for Histogram and Tabular representations size_t order_dim = get_order(); - xt::xtensor matrix({groups, groups, order_dim}, 0); + tensor::Tensor matrix({groups, groups, order_dim}, 0); for (int gin = 0; gin < groups; gin++) { for (int i_gout = 0; i_gout < energy[gin].size(); i_gout++) { @@ -600,8 +603,8 @@ void ScattDataHistogram::combine( size_t groups = those_scatts[0]->energy.size(); - xt::xtensor in_gmin({groups}, 0); - xt::xtensor in_gmax({groups}, 0); + tensor::Tensor in_gmin({groups}, 0); + tensor::Tensor in_gmax({groups}, 0); double_3dvec sparse_scatter(groups); double_2dvec sparse_mult(groups); @@ -620,8 +623,8 @@ void ScattDataHistogram::combine( // ScattDataTabular methods //============================================================================== -void ScattDataTabular::init(const xt::xtensor& in_gmin, - const xt::xtensor& in_gmax, const double_2dvec& in_mult, +void ScattDataTabular::init(const tensor::Tensor& in_gmin, + const tensor::Tensor& in_gmax, const double_2dvec& in_mult, const double_3dvec& coeffs) { size_t groups = coeffs.size(); @@ -631,12 +634,12 @@ void ScattDataTabular::init(const xt::xtensor& in_gmin, double_3dvec matrix = coeffs; // Build the angular distribution mu values - mu = xt::linspace(-1., 1., order); + mu = tensor::linspace(-1., 1., order); dmu = 2. / (order - 1); // Get the scattering cross section value by integrating the distribution // over all mu points and then combining over all outgoing groups - scattxs = xt::zeros({groups}); + scattxs = tensor::zeros({groups}); for (int gin = 0; gin < groups; gin++) { for (int i_gout = 0; i_gout < matrix[gin].size(); i_gout++) { for (int imu = 1; imu < order; imu++) { @@ -713,7 +716,7 @@ double ScattDataTabular::calc_f(int gin, int gout, double mu) int imu; if (mu == 1.) { // use size -2 to have the index one before the end - imu = this->mu.shape()[0] - 2; + imu = this->mu.shape(0) - 2; } else { imu = std::floor((mu + 1.) / dmu + 1.) - 1; } @@ -734,7 +737,7 @@ void ScattDataTabular::sample( sample_energy(gin, gout, i_gout, seed); // Determine the outgoing cosine bin - int NP = this->mu.shape()[0]; + int NP = this->mu.shape(0); double xi = prn(seed); double c_k = dist[gin][i_gout][0]; @@ -776,13 +779,14 @@ void ScattDataTabular::sample( //============================================================================== -xt::xtensor ScattDataTabular::get_matrix(size_t max_order) +tensor::Tensor ScattDataTabular::get_matrix(size_t max_order) { // Get the sizes and initialize the data to 0 size_t groups = energy.size(); // We ignore the requested order for Histogram and Tabular representations size_t order_dim = get_order(); - xt::xtensor matrix({groups, groups, order_dim}, 0.); + tensor::Tensor matrix = + tensor::zeros({groups, groups, order_dim}); for (int gin = 0; gin < groups; gin++) { for (int i_gout = 0; i_gout < energy[gin].size(); i_gout++) { @@ -816,8 +820,8 @@ void ScattDataTabular::combine( size_t groups = those_scatts[0]->energy.size(); - xt::xtensor in_gmin({groups}, 0); - xt::xtensor in_gmax({groups}, 0); + tensor::Tensor in_gmin({groups}, 0); + tensor::Tensor in_gmax({groups}, 0); double_3dvec sparse_scatter(groups); double_2dvec sparse_mult(groups); @@ -854,7 +858,7 @@ void convert_legendre_to_tabular(ScattDataLegendre& leg, ScattDataTabular& tab) tab.scattxs = leg.scattxs; // Build mu and dmu - tab.mu = xt::linspace(-1., 1., n_mu); + tab.mu = tensor::linspace(-1., 1., n_mu); tab.dmu = 2. / (n_mu - 1); // Calculate f(mu) and integrate it so we can avoid rejection sampling diff --git a/src/secondary_correlated.cpp b/src/secondary_correlated.cpp index e820419b48..cc0ab8af19 100644 --- a/src/secondary_correlated.cpp +++ b/src/secondary_correlated.cpp @@ -5,8 +5,7 @@ #include // for size_t #include // for back_inserter -#include "xtensor/xarray.hpp" -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include "openmc/endf.h" #include "openmc/hdf5_interface.h" @@ -26,11 +25,11 @@ CorrelatedAngleEnergy::CorrelatedAngleEnergy(hid_t group) hid_t dset = open_dataset(group, "energy"); // Get interpolation parameters - xt::xarray temp; + tensor::Tensor temp; read_attribute(dset, "interpolation", temp); - auto temp_b = xt::view(temp, 0); // view of breakpoints - auto temp_i = xt::view(temp, 1); // view of interpolation parameters + tensor::View temp_b = temp.slice(0); // breakpoints + tensor::View temp_i = temp.slice(1); // interpolation parameters std::copy(temp_b.begin(), temp_b.end(), std::back_inserter(breakpoints_)); for (const auto i : temp_i) @@ -51,12 +50,12 @@ CorrelatedAngleEnergy::CorrelatedAngleEnergy(hid_t group) read_attribute(dset, "interpolation", interp); read_attribute(dset, "n_discrete_lines", n_discrete); - xt::xarray eout; + tensor::Tensor eout; read_dataset(dset, eout); close_dataset(dset); // Read angle distributions - xt::xarray mu; + tensor::Tensor mu; read_dataset(group, "mu", mu); for (int i = 0; i < n_energy; ++i) { @@ -66,7 +65,7 @@ CorrelatedAngleEnergy::CorrelatedAngleEnergy(hid_t group) if (i < n_energy - 1) { n = offsets[i + 1] - j; } else { - n = eout.shape()[1] - j; + n = eout.shape(1) - j; } // Assign interpolation scheme and number of discrete lines @@ -75,9 +74,9 @@ CorrelatedAngleEnergy::CorrelatedAngleEnergy(hid_t group) d.n_discrete = n_discrete[i]; // Copy data - d.e_out = xt::view(eout, 0, xt::range(j, j + n)); - d.p = xt::view(eout, 1, xt::range(j, j + n)); - d.c = xt::view(eout, 2, xt::range(j, j + n)); + d.e_out = eout.slice(0, tensor::range(j, j + n)); + d.p = eout.slice(1, tensor::range(j, j + n)); + d.c = eout.slice(2, tensor::range(j, j + n)); // To get answers that match ACE data, for now we still use the tabulated // CDF values that were passed through to the HDF5 library. At a later @@ -119,10 +118,10 @@ CorrelatedAngleEnergy::CorrelatedAngleEnergy(hid_t group) // Determine offset and size of distribution int offset_mu = std::lround(eout(4, offsets[i] + j)); int m; - if (offsets[i] + j + 1 < eout.shape()[1]) { + if (offsets[i] + j + 1 < eout.shape(1)) { m = std::lround(eout(4, offsets[i] + j + 1)) - offset_mu; } else { - m = mu.shape()[1] - offset_mu; + m = mu.shape(1) - offset_mu; } // For incoherent inelastic thermal scattering, the angle distributions @@ -133,9 +132,12 @@ CorrelatedAngleEnergy::CorrelatedAngleEnergy(hid_t group) interp_mu = 1; auto interp = int2interp(interp_mu); - auto xs = xt::view(mu, 0, xt::range(offset_mu, offset_mu + m)); - auto ps = xt::view(mu, 1, xt::range(offset_mu, offset_mu + m)); - auto cs = xt::view(mu, 2, xt::range(offset_mu, offset_mu + m)); + tensor::View xs = + mu.slice(0, tensor::range(offset_mu, offset_mu + m)); + tensor::View ps = + mu.slice(1, tensor::range(offset_mu, offset_mu + m)); + tensor::View cs = + mu.slice(2, tensor::range(offset_mu, offset_mu + m)); vector x {xs.begin(), xs.end()}; vector p {ps.begin(), ps.end()}; diff --git a/src/secondary_kalbach.cpp b/src/secondary_kalbach.cpp index 6ac91e665b..8470c8c18e 100644 --- a/src/secondary_kalbach.cpp +++ b/src/secondary_kalbach.cpp @@ -5,8 +5,7 @@ #include // for size_t #include // for back_inserter -#include "xtensor/xarray.hpp" -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include "openmc/hdf5_interface.h" #include "openmc/math_functions.h" @@ -27,11 +26,11 @@ KalbachMann::KalbachMann(hid_t group) hid_t dset = open_dataset(group, "energy"); // Get interpolation parameters - xt::xarray temp; + tensor::Tensor temp; read_attribute(dset, "interpolation", temp); - auto temp_b = xt::view(temp, 0); // view of breakpoints - auto temp_i = xt::view(temp, 1); // view of interpolation parameters + tensor::View temp_b = temp.slice(0); // breakpoints + tensor::View temp_i = temp.slice(1); // interpolation parameters std::copy(temp_b.begin(), temp_b.end(), std::back_inserter(breakpoints_)); for (const auto i : temp_i) @@ -52,7 +51,7 @@ KalbachMann::KalbachMann(hid_t group) read_attribute(dset, "interpolation", interp); read_attribute(dset, "n_discrete_lines", n_discrete); - xt::xarray eout; + tensor::Tensor eout; read_dataset(dset, eout); close_dataset(dset); @@ -63,7 +62,7 @@ KalbachMann::KalbachMann(hid_t group) if (i < n_energy - 1) { n = offsets[i + 1] - j; } else { - n = eout.shape()[1] - j; + n = eout.shape(1) - j; } // Assign interpolation scheme and number of discrete lines @@ -72,11 +71,11 @@ KalbachMann::KalbachMann(hid_t group) d.n_discrete = n_discrete[i]; // Copy data - d.e_out = xt::view(eout, 0, xt::range(j, j + n)); - d.p = xt::view(eout, 1, xt::range(j, j + n)); - d.c = xt::view(eout, 2, xt::range(j, j + n)); - d.r = xt::view(eout, 3, xt::range(j, j + n)); - d.a = xt::view(eout, 4, xt::range(j, j + n)); + d.e_out = eout.slice(0, tensor::range(j, j + n)); + d.p = eout.slice(1, tensor::range(j, j + n)); + d.c = eout.slice(2, tensor::range(j, j + n)); + d.r = eout.slice(3, tensor::range(j, j + n)); + d.a = eout.slice(4, tensor::range(j, j + n)); // To get answers that match ACE data, for now we still use the tabulated // CDF values that were passed through to the HDF5 library. At a later diff --git a/src/secondary_thermal.cpp b/src/secondary_thermal.cpp index 030d398aab..2ab7d8a63e 100644 --- a/src/secondary_thermal.cpp +++ b/src/secondary_thermal.cpp @@ -5,7 +5,7 @@ #include "openmc/random_lcg.h" #include "openmc/search.h" -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include #include // for log, exp @@ -85,7 +85,7 @@ void IncoherentElasticAEDiscrete::sample( // incoming energies. // Sample outgoing cosine bin - int n_mu = mu_out_.shape()[1]; + int n_mu = mu_out_.shape(1); int k = prn(seed) * n_mu; // Rather than use the sampled discrete mu directly, it is smeared over @@ -145,7 +145,7 @@ void IncoherentInelasticAEDiscrete::sample( // probability of 1). Otherwise, each bin is equally probable. int j; - int n = energy_out_.shape()[1]; + int n = energy_out_.shape(1); if (!skewed_) { // All bins equally likely j = prn(seed) * n; @@ -178,7 +178,7 @@ void IncoherentInelasticAEDiscrete::sample( E_out = (1 - f) * E_ij + f * E_i1j; // Sample outgoing cosine bin - int m = mu_out_.shape()[2]; + int m = mu_out_.shape(2); int k = prn(seed) * m; // Determine outgoing cosine corresponding to E_in[i] and E_in[i+1] @@ -218,11 +218,11 @@ IncoherentInelasticAE::IncoherentInelasticAE(hid_t group) // On first pass, allocate space for angles if (j == 0) { auto n_mu = adist->x().size(); - d.mu = xt::empty({d.n_e_out, n_mu}); + d.mu = tensor::Tensor({d.n_e_out, n_mu}); } // Copy outgoing angles - auto mu_j = xt::view(d.mu, j); + tensor::View mu_j = d.mu.slice(j); std::copy(adist->x().begin(), adist->x().end(), mu_j.begin()); } } @@ -287,7 +287,7 @@ void IncoherentInelasticAE::sample( } // Sample outgoing cosine bin - int n_mu = distribution_[l].mu.shape()[1]; + int n_mu = distribution_[l].mu.shape(1); std::size_t k = prn(seed) * n_mu; // Rather than use the sampled discrete mu directly, it is smeared over diff --git a/src/simulation.cpp b/src/simulation.cpp index 18e40a8bc7..d0d6f037f9 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -30,7 +30,7 @@ #ifdef _OPENMP #include #endif -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #ifdef OPENMC_MPI #include @@ -413,7 +413,7 @@ void finalize_batch() // Reset global tally results if (simulation::current_batch <= settings::n_inactive) { - xt::view(simulation::global_tallies, xt::all()) = 0.0; + simulation::global_tallies.fill(0.0); simulation::n_realizations = 0; } diff --git a/src/source.cpp b/src/source.cpp index 8bd9c78935..5300a685f7 100644 --- a/src/source.cpp +++ b/src/source.cpp @@ -10,7 +10,7 @@ #include // for dlopen, dlsym, dlclose, dlerror #endif -#include "xtensor/xadapt.hpp" +#include "openmc/tensor.h" #include #include "openmc/bank.h" @@ -400,8 +400,9 @@ SourceSite IndependentSource::sample(uint64_t* seed) const auto p = particle_.transport_index(); auto energy_ptr = dynamic_cast(energy_.get()); if (energy_ptr) { - auto energies = xt::adapt(energy_ptr->x()); - if (xt::any(energies > data::energy_max[p])) { + auto energies = + tensor::Tensor(energy_ptr->x().data(), energy_ptr->x().size()); + if ((energies > data::energy_max[p]).any()) { fatal_error("Source energy above range of energies of at least " "one cross section table"); } diff --git a/src/state_point.cpp b/src/state_point.cpp index 455299529b..c0d8ab5b2f 100644 --- a/src/state_point.cpp +++ b/src/state_point.cpp @@ -4,8 +4,7 @@ #include // for int64_t #include -#include "xtensor/xbuilder.hpp" // for empty_like -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include #include "openmc/bank.h" @@ -277,8 +276,8 @@ extern "C" int openmc_statepoint_write(const char* filename, bool* write_source) std::string name = "tally " + std::to_string(tally->id_); hid_t tally_group = open_group(tallies_group, name.c_str()); auto& results = tally->results_; - write_tally_results(tally_group, results.shape()[0], - results.shape()[1], results.shape()[2], results.data()); + write_tally_results(tally_group, results.shape(0), results.shape(1), + results.shape(2), results.data()); close_group(tally_group); } } else { @@ -517,8 +516,8 @@ extern "C" int openmc_statepoint_load(const char* filename) tally->writable_ = false; } else { auto& results = tally->results_; - read_tally_results(tally_group, results.shape()[0], - results.shape()[1], results.shape()[2], results.data()); + read_tally_results(tally_group, results.shape(0), results.shape(1), + results.shape(2), results.data()); read_dataset(tally_group, "n_realizations", tally->n_realizations_); close_group(tally_group); @@ -827,7 +826,7 @@ void write_unstructured_mesh_results() // construct result vectors vector mean_vec(umesh->n_bins()), std_dev_vec(umesh->n_bins()); - for (int j = 0; j < tally->results_.shape()[0]; j++) { + for (int j = 0; j < tally->results_.shape(0); j++) { // get the volume for this bin double volume = umesh->volume(j); // compute the mean @@ -889,7 +888,7 @@ void write_tally_results_nr(hid_t file_id) #ifdef OPENMC_MPI // Reduce global tallies - xt::xtensor gt_reduced = xt::empty_like(gt); + tensor::Tensor gt_reduced({N_GLOBAL_TALLIES, 3}); MPI_Reduce(gt.data(), gt_reduced.data(), gt.size(), MPI_DOUBLE, MPI_SUM, 0, mpi::intracomm); @@ -918,13 +917,18 @@ void write_tally_results_nr(hid_t file_id) write_attribute(file_id, "tallies_present", 1); } - // Get view of accumulated tally values - auto values_view = xt::view(t->results_, xt::all(), xt::all(), - xt::range(static_cast(TallyResult::SUM), - static_cast(TallyResult::SUM_SQ) + 1)); - - // Make copy of tally values in contiguous array - xt::xtensor values = values_view; + // Copy the SUM and SUM_SQ columns from the tally results into a + // contiguous array for MPI reduction + const int r_start = static_cast(TallyResult::SUM); + const int r_end = static_cast(TallyResult::SUM_SQ) + 1; + const size_t r_count = r_end - r_start; + const size_t ni = t->results_.shape(0); + const size_t nj = t->results_.shape(1); + tensor::Tensor values({ni, nj, r_count}); + for (size_t i = 0; i < ni; i++) + for (size_t j = 0; j < nj; j++) + for (size_t r = 0; r < r_count; r++) + values(i, j, r) = t->results_(i, j, r_start + r); if (mpi::master) { // Open group for tally @@ -938,19 +942,22 @@ void write_tally_results_nr(hid_t file_id) MPI_SUM, 0, mpi::intracomm); #endif - // At the end of the simulation, store the results back in the - // regular TallyResults array + // At the end of the simulation, store the reduced results back + // into the tally results array if (simulation::current_batch == settings::n_max_batches || simulation::satisfy_triggers) { - values_view = values; + for (size_t i = 0; i < ni; i++) + for (size_t j = 0; j < nj; j++) + for (size_t r = 0; r < r_count; r++) + t->results_(i, j, r_start + r) = values(i, j, r); } - // Put in temporary tally result - xt::xtensor results_copy = xt::zeros_like(t->results_); - auto copy_view = xt::view(results_copy, xt::all(), xt::all(), - xt::range(static_cast(TallyResult::SUM), - static_cast(TallyResult::SUM_SQ) + 1)); - copy_view = values; + // Put reduced values into a full-sized copy for writing to HDF5 + tensor::Tensor results_copy = tensor::zeros_like(t->results_); + for (size_t i = 0; i < ni; i++) + for (size_t j = 0; j < nj; j++) + for (size_t r = 0; r < r_count; r++) + results_copy(i, j, r_start + r) = values(i, j, r); // Write reduced tally results to file auto shape = results_copy.shape(); diff --git a/src/tallies/filter_cell_instance.cpp b/src/tallies/filter_cell_instance.cpp index 316a758d11..928bfb6c5e 100644 --- a/src/tallies/filter_cell_instance.cpp +++ b/src/tallies/filter_cell_instance.cpp @@ -9,6 +9,7 @@ #include "openmc/cell.h" #include "openmc/error.h" #include "openmc/geometry.h" +#include "openmc/tensor.h" #include "openmc/xml_interface.h" namespace openmc { @@ -108,7 +109,7 @@ void CellInstanceFilter::to_statepoint(hid_t filter_group) const { Filter::to_statepoint(filter_group); size_t n = cell_instances_.size(); - xt::xtensor data({n, 2}); + tensor::Tensor data({n, 2}); for (int64_t i = 0; i < n; ++i) { const auto& x = cell_instances_[i]; data(i, 0) = model::cells[x.index_cell]->id_; diff --git a/src/tallies/filter_meshmaterial.cpp b/src/tallies/filter_meshmaterial.cpp index 6e1f30380f..b45bb4164e 100644 --- a/src/tallies/filter_meshmaterial.cpp +++ b/src/tallies/filter_meshmaterial.cpp @@ -1,5 +1,6 @@ #include "openmc/tallies/filter_meshmaterial.h" +#include #include // for move #include @@ -10,6 +11,7 @@ #include "openmc/error.h" #include "openmc/material.h" #include "openmc/mesh.h" +#include "openmc/tensor.h" #include "openmc/xml_interface.h" namespace openmc { @@ -161,7 +163,7 @@ void MeshMaterialFilter::to_statepoint(hid_t filter_group) const write_dataset(filter_group, "mesh", model::meshes[mesh_]->id_); size_t n = bins_.size(); - xt::xtensor data({n, 2}); + tensor::Tensor data({n, 2}); for (int64_t i = 0; i < n; ++i) { const auto& x = bins_[i]; data(i, 0) = x.index_element; diff --git a/src/tallies/tally.cpp b/src/tallies/tally.cpp index 8cdf3a9050..296c3fb505 100644 --- a/src/tallies/tally.cpp +++ b/src/tallies/tally.cpp @@ -35,9 +35,7 @@ #include "openmc/tallies/filter_time.h" #include "openmc/xml_interface.h" -#include "xtensor/xadapt.hpp" -#include "xtensor/xbuilder.hpp" // for empty_like -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include #include // for max, set_union @@ -69,7 +67,7 @@ vector time_grid; } // namespace model namespace simulation { -xt::xtensor_fixed> global_tallies; +tensor::StaticTensor2D global_tallies; int32_t n_realizations {0}; } // namespace simulation @@ -806,9 +804,11 @@ void Tally::init_results() { int n_scores = scores_.size() * nuclides_.size(); if (higher_moments_) { - results_ = xt::empty({n_filter_bins_, n_scores, 5}); + results_ = tensor::Tensor({static_cast(n_filter_bins_), + static_cast(n_scores), size_t {5}}); } else { - results_ = xt::empty({n_filter_bins_, n_scores, 3}); + results_ = tensor::Tensor({static_cast(n_filter_bins_), + static_cast(n_scores), size_t {3}}); } } @@ -816,7 +816,7 @@ void Tally::reset() { n_realizations_ = 0; if (results_.size() != 0) { - xt::view(results_, xt::all()) = 0.0; + results_.fill(0.0); } } @@ -851,9 +851,9 @@ void Tally::accumulate() if (higher_moments_) { #pragma omp parallel for // filter bins (specific cell, energy bins) - for (int i = 0; i < results_.shape()[0]; ++i) { + for (int i = 0; i < results_.shape(0); ++i) { // score bins (flux, total reaction rate, fission reaction rate, etc.) - for (int j = 0; j < results_.shape()[1]; ++j) { + for (int j = 0; j < results_.shape(1); ++j) { double val = results_(i, j, TallyResult::VALUE) * norm; double val2 = val * val; results_(i, j, TallyResult::VALUE) = 0.0; @@ -866,9 +866,9 @@ void Tally::accumulate() } else { #pragma omp parallel for // filter bins (specific cell, energy bins) - for (int i = 0; i < results_.shape()[0]; ++i) { + for (int i = 0; i < results_.shape(0); ++i) { // score bins (flux, total reaction rate, fission reaction rate, etc.) - for (int j = 0; j < results_.shape()[1]; ++j) { + for (int j = 0; j < results_.shape(1); ++j) { double val = results_(i, j, TallyResult::VALUE) * norm; results_(i, j, TallyResult::VALUE) = 0.0; results_(i, j, TallyResult::SUM) += val; @@ -888,18 +888,18 @@ int Tally::score_index(const std::string& score) const return -1; } -xt::xarray Tally::get_reshaped_data() const +tensor::Tensor Tally::get_reshaped_data() const { - std::vector shape; + vector shape; for (auto f : filters()) { shape.push_back(model::tally_filters[f]->n_bins()); } // add number of scores and nuclides to tally - shape.push_back(results_.shape()[1]); - shape.push_back(results_.shape()[2]); + shape.push_back(results_.shape(1)); + shape.push_back(results_.shape(2)); - xt::xarray reshaped_results = results_; + tensor::Tensor reshaped_results = results_; reshaped_results.reshape(shape); return reshaped_results; } @@ -1004,13 +1004,14 @@ void reduce_tally_results() // Skip any tallies that are not active auto& tally {model::tallies[i_tally]}; - // Get view of accumulated tally values - auto values_view = xt::view(tally->results_, xt::all(), xt::all(), - static_cast(TallyResult::VALUE)); + // Extract 2D view of the VALUE column from the 3D results tensor, + // then copy into a contiguous array for MPI reduction + const int val_idx = static_cast(TallyResult::VALUE); + tensor::View val_view = + tally->results_.slice(tensor::all, tensor::all, val_idx); + tensor::Tensor values(val_view); - // Make copy of tally values in contiguous array - xt::xtensor values = values_view; - xt::xtensor values_reduced = xt::empty_like(values); + tensor::Tensor values_reduced(values.shape()); // Reduce contiguous set of tally results MPI_Reduce(values.data(), values_reduced.data(), values.size(), @@ -1018,9 +1019,9 @@ void reduce_tally_results() // Transfer values on master and reset on other ranks if (mpi::master) { - values_view = values_reduced; + val_view = values_reduced; } else { - values_view = 0.0; + val_view = 0.0; } } } @@ -1028,14 +1029,13 @@ void reduce_tally_results() // Note that global tallies are *always* reduced even when no_reduce option // is on. - // Get view of global tally values + // Get reference to global tallies auto& gt = simulation::global_tallies; - auto gt_values_view = - xt::view(gt, xt::all(), static_cast(TallyResult::VALUE)); + const int val_col = static_cast(TallyResult::VALUE); - // Make copy of values in contiguous array - xt::xtensor gt_values = gt_values_view; - xt::xtensor gt_values_reduced = xt::empty_like(gt_values); + // Copy VALUE column into contiguous array for MPI reduction + tensor::Tensor gt_values(gt.slice(tensor::all, val_col)); + tensor::Tensor gt_values_reduced({size_t {N_GLOBAL_TALLIES}}); // Reduce contiguous data MPI_Reduce(gt_values.data(), gt_values_reduced.data(), N_GLOBAL_TALLIES, @@ -1043,9 +1043,9 @@ void reduce_tally_results() // Transfer values on master and reset on other ranks if (mpi::master) { - gt_values_view = gt_values_reduced; + gt.slice(tensor::all, val_col) = gt_values_reduced; } else { - gt_values_view = 0.0; + gt.slice(tensor::all, val_col) = 0.0; } // We also need to determine the total starting weight of particles from the diff --git a/src/tallies/tally_scoring.cpp b/src/tallies/tally_scoring.cpp index ccc70fc8f1..fb851f7838 100644 --- a/src/tallies/tally_scoring.cpp +++ b/src/tallies/tally_scoring.cpp @@ -20,6 +20,7 @@ #include "openmc/tallies/filter_delayedgroup.h" #include "openmc/tallies/filter_energy.h" +#include #include namespace openmc { diff --git a/src/tallies/trigger.cpp b/src/tallies/trigger.cpp index f1f83e2982..6c54edd5e1 100644 --- a/src/tallies/trigger.cpp +++ b/src/tallies/trigger.cpp @@ -71,7 +71,7 @@ void check_tally_triggers(double& ratio, int& tally_id, int& score) continue; const auto& results = t.results_; - for (auto filter_index = 0; filter_index < results.shape()[0]; + for (auto filter_index = 0; filter_index < results.shape(0); ++filter_index) { // Compute the tally uncertainty metrics. auto uncert_pair = diff --git a/src/thermal.cpp b/src/thermal.cpp index cbe0983ed6..6ed59f6869 100644 --- a/src/thermal.cpp +++ b/src/thermal.cpp @@ -3,12 +3,7 @@ #include // for sort, move, min, max, find #include // for round, sqrt, abs -#include "xtensor/xarray.hpp" -#include "xtensor/xbuilder.hpp" -#include "xtensor/xmath.hpp" -#include "xtensor/xsort.hpp" -#include "xtensor/xtensor.hpp" -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include #include "openmc/constants.h" @@ -55,7 +50,7 @@ ThermalScattering::ThermalScattering( // Determine temperatures available auto dset_names = dataset_names(kT_group); auto n = dset_names.size(); - auto temps_available = xt::empty({n}); + auto temps_available = tensor::Tensor({n}); for (int i = 0; i < dset_names.size(); ++i) { // Read temperature value double T; @@ -82,7 +77,7 @@ ThermalScattering::ThermalScattering( // Determine actual temperatures to read for (const auto& T : temperature) { - auto i_closest = xt::argmin(xt::abs(temps_available - T))[0]; + auto i_closest = tensor::abs(temps_available - T).argmin(); auto temp_actual = temps_available[i_closest]; if (std::abs(temp_actual - T) < settings::temperature_tolerance) { if (std::find(temps_to_read.begin(), temps_to_read.end(), diff --git a/src/track_output.cpp b/src/track_output.cpp index e86f774f04..02e0922364 100644 --- a/src/track_output.cpp +++ b/src/track_output.cpp @@ -8,7 +8,7 @@ #include "openmc/simulation.h" #include "openmc/vector.h" -#include "xtensor/xtensor.hpp" +#include "openmc/tensor.h" #include #include diff --git a/src/urr.cpp b/src/urr.cpp index 02cef22809..b791eecb0d 100644 --- a/src/urr.cpp +++ b/src/urr.cpp @@ -25,7 +25,7 @@ UrrData::UrrData(hid_t group_id) // Read URR tables. The HDF5 format is a little // different from how we want it laid out in memory. // This array used to be called "prob_". - xt::xtensor tmp_prob; + tensor::Tensor tmp_prob; read_dataset(group_id, "table", tmp_prob); auto shape = tmp_prob.shape(); @@ -38,7 +38,7 @@ UrrData::UrrData(hid_t group_id) xs_values_.resize({n_energy, n_cdf_values}); // Now fill in the values. Using manual loops here since we might - // not have fancy xtensor slicing code written for GPU tensors. + // not have fancy tensor slicing code written for GPU tensors. // The below enum gives how URR tables are laid out in our HDF5 tables. enum class URRTableParam { CUM_PROB, diff --git a/src/volume_calc.cpp b/src/volume_calc.cpp index 1deffb8048..f675ae78a2 100644 --- a/src/volume_calc.cpp +++ b/src/volume_calc.cpp @@ -17,8 +17,7 @@ #include "openmc/timer.h" #include "openmc/xml_interface.h" -#include "xtensor/xadapt.hpp" -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include #include // for copy @@ -242,7 +241,8 @@ vector VolumeCalculation::execute() const // non-zero auto n_nuc = settings::run_CE ? data::nuclides.size() : data::mg.nuclides_.size(); - xt::xtensor atoms({n_nuc, 2}, 0.0); + auto atoms = + tensor::zeros({static_cast(n_nuc), size_t {2}}); #ifdef OPENMC_MPI if (mpi::master) { @@ -452,9 +452,11 @@ void VolumeCalculation::to_hdf5( } // Create array of total # of atoms with uncertainty for each nuclide - xt::xtensor atom_data({n_nuc, 2}); - xt::view(atom_data, xt::all(), 0) = xt::adapt(result.atoms); - xt::view(atom_data, xt::all(), 1) = xt::adapt(result.uncertainty); + tensor::Tensor atom_data({static_cast(n_nuc), size_t {2}}); + for (size_t k = 0; k < static_cast(n_nuc); ++k) { + atom_data(k, 0) = result.atoms[k]; + atom_data(k, 1) = result.uncertainty[k]; + } // Write results write_dataset(group_id, "nuclides", nucnames); diff --git a/src/weight_windows.cpp b/src/weight_windows.cpp index 5ca4addbbf..c051691320 100644 --- a/src/weight_windows.cpp +++ b/src/weight_windows.cpp @@ -6,12 +6,7 @@ #include #include -#include "xtensor/xdynamic_view.hpp" -#include "xtensor/xindex_view.hpp" -#include "xtensor/xio.hpp" -#include "xtensor/xmasked_view.hpp" -#include "xtensor/xnoalias.hpp" -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include "openmc/error.h" #include "openmc/file_utils.h" @@ -265,8 +260,12 @@ WeightWindows* WeightWindows::from_hdf5( } wws->set_mesh(model::mesh_map[mesh_id]); - wws->lower_ww_ = xt::empty(wws->bounds_size()); - wws->upper_ww_ = xt::empty(wws->bounds_size()); + wws->lower_ww_ = + tensor::Tensor({static_cast(wws->bounds_size()[0]), + static_cast(wws->bounds_size()[1])}); + wws->upper_ww_ = + tensor::Tensor({static_cast(wws->bounds_size()[0]), + static_cast(wws->bounds_size()[1])}); read_dataset(ww_group, "lower_ww_bounds", wws->lower_ww_); read_dataset(ww_group, "upper_ww_bounds", wws->upper_ww_); @@ -301,9 +300,11 @@ void WeightWindows::allocate_ww_bounds() "Size of weight window bounds is zero for WeightWindows {}", id()); warning(msg); } - lower_ww_ = xt::empty(shape); + lower_ww_ = tensor::Tensor( + {static_cast(shape[0]), static_cast(shape[1])}); lower_ww_.fill(-1); - upper_ww_ = xt::empty(shape); + upper_ww_ = tensor::Tensor( + {static_cast(shape[0]), static_cast(shape[1])}); upper_ww_.fill(-1); } @@ -448,8 +449,8 @@ void WeightWindows::check_bounds(const T& bounds) const } } -void WeightWindows::set_bounds(const xt::xtensor& lower_bounds, - const xt::xtensor& upper_bounds) +void WeightWindows::set_bounds(const tensor::Tensor& lower_bounds, + const tensor::Tensor& upper_bounds) { this->check_bounds(lower_bounds, upper_bounds); @@ -460,7 +461,7 @@ void WeightWindows::set_bounds(const xt::xtensor& lower_bounds, } void WeightWindows::set_bounds( - const xt::xtensor& lower_bounds, double ratio) + const tensor::Tensor& lower_bounds, double ratio) { this->check_bounds(lower_bounds); @@ -475,14 +476,16 @@ void WeightWindows::set_bounds( { check_bounds(lower_bounds, upper_bounds); auto shape = this->bounds_size(); - lower_ww_ = xt::empty(shape); - upper_ww_ = xt::empty(shape); + lower_ww_ = tensor::Tensor( + {static_cast(shape[0]), static_cast(shape[1])}); + upper_ww_ = tensor::Tensor( + {static_cast(shape[0]), static_cast(shape[1])}); - // set new weight window values - xt::view(lower_ww_, xt::all()) = - xt::adapt(lower_bounds.data(), lower_ww_.shape()); - xt::view(upper_ww_, xt::all()) = - xt::adapt(upper_bounds.data(), upper_ww_.shape()); + // Copy weight window values from input spans into the tensors + std::copy(lower_bounds.data(), lower_bounds.data() + lower_ww_.size(), + lower_ww_.data()); + std::copy(upper_bounds.data(), upper_bounds.data() + upper_ww_.size(), + upper_ww_.data()); } void WeightWindows::set_bounds(span lower_bounds, double ratio) @@ -490,14 +493,16 @@ void WeightWindows::set_bounds(span lower_bounds, double ratio) this->check_bounds(lower_bounds); auto shape = this->bounds_size(); - lower_ww_ = xt::empty(shape); - upper_ww_ = xt::empty(shape); + lower_ww_ = tensor::Tensor( + {static_cast(shape[0]), static_cast(shape[1])}); + upper_ww_ = tensor::Tensor( + {static_cast(shape[0]), static_cast(shape[1])}); - // set new weight window values - xt::view(lower_ww_, xt::all()) = - xt::adapt(lower_bounds.data(), lower_ww_.shape()); - xt::view(upper_ww_, xt::all()) = - xt::adapt(lower_bounds.data(), upper_ww_.shape()); + // Copy lower bounds into both arrays, then scale upper by ratio + std::copy(lower_bounds.data(), lower_bounds.data() + lower_ww_.size(), + lower_ww_.data()); + std::copy(lower_bounds.data(), lower_bounds.data() + upper_ww_.size(), + upper_ww_.data()); upper_ww_ *= ratio; } @@ -510,8 +515,8 @@ void WeightWindows::update_weights(const Tally* tally, const std::string& value, this->check_tally_update_compatibility(tally); // Dimensions of weight window arrays - int e_bins = lower_ww_.shape()[0]; - int64_t mesh_bins = lower_ww_.shape()[1]; + int e_bins = lower_ww_.shape(0); + int64_t mesh_bins = lower_ww_.shape(1); // Initialize weight window arrays to -1.0 by default #pragma omp parallel for collapse(2) schedule(static) @@ -542,16 +547,16 @@ void WeightWindows::update_weights(const Tally* tally, const std::string& value, /////////////////////////// // Extract tally data // - // At the end of this section, the mean and rel_err array - // is a 2D view of tally data (n_e_groups, n_mesh_bins) + // At the end of this section, mean and rel_err are + // 2D tensors of tally data (n_e_groups, n_mesh_bins) // /////////////////////////// - // build a shape for a view of the tally results, this will always be + // build a shape for the tally results, this will always be // dimension 5 (3 filter dimensions, 1 score dimension, 1 results dimension) - // Look for the size of the last dimension of the results array - const auto& results_arr = tally->results(); - const int results_dim = static_cast(results_arr.shape()[2]); + // Look for the size of the last dimension of the results tensor + const auto& results = tally->results(); + const int results_dim = static_cast(results.shape(2)); std::array shape = {1, 1, 1, tally->n_scores(), results_dim}; // set the shape for the filters applied on the tally @@ -588,25 +593,14 @@ void WeightWindows::update_weights(const Tally* tally, const std::string& value, std::find(filter_types.begin(), filter_types.end(), FilterType::MESH) - filter_types.begin(); - // get a fully reshaped view of the tally according to tally ordering of - // filters - auto tally_values = xt::reshape_view(results_arr, shape); - - // get a that is (particle, energy, mesh, scores, values) - auto transposed_view = xt::transpose(tally_values, transpose); - - // determine the dimension and index of the particle data + // determine the index of the particle within its filter int particle_idx = 0; if (tally->has_filter(FilterType::PARTICLE)) { - // get the particle filter auto pf = tally->get_filter(); const auto& particles = pf->particles(); - // find the index of the particle that matches these weight windows auto p_it = std::find(particles.begin(), particles.end(), this->particle_type_); - // if the particle filter doesn't have particle data for the particle - // used on this weight windows instance, report an error if (p_it == particles.end()) { auto msg = fmt::format("Particle type '{}' not present on Filter {} for " "Tally {} used to update WeightWindows {}", @@ -614,17 +608,46 @@ void WeightWindows::update_weights(const Tally* tally, const std::string& value, fatal_error(msg); } - // use the index of the particle in the filter to down-select data later particle_idx = p_it - particles.begin(); } - // down-select data based on particle and score - auto sum = xt::dynamic_view( - transposed_view, {particle_idx, xt::all(), xt::all(), score_index, - static_cast(TallyResult::SUM)}); - auto sum_sq = xt::dynamic_view( - transposed_view, {particle_idx, xt::all(), xt::all(), score_index, - static_cast(TallyResult::SUM_SQ)}); + // The tally results array is 3D: (n_filter_combos, n_scores, n_result_types). + // The first dimension is a row-major flattening of up to 3 filter dimensions + // (particle, energy, mesh) whose storage order depends on which filters the + // tally has. We need to map our desired indices (particle, energy, mesh) + // into the correct flat filter combination index. + // + // transpose[i] tells us which storage position holds dimension i: + // i=0 -> particle, i=1 -> energy, i=2 -> mesh + // shape[j] gives the number of bins for filter storage position j. + + // Row-major strides for the 3 filter dimensions + const int stride0 = shape[1] * shape[2]; + const int stride1 = shape[2]; + + tensor::Tensor sum( + {static_cast(e_bins), static_cast(mesh_bins)}); + tensor::Tensor sum_sq( + {static_cast(e_bins), static_cast(mesh_bins)}); + + const int i_sum = static_cast(TallyResult::SUM); + const int i_sum_sq = static_cast(TallyResult::SUM_SQ); + + for (int e = 0; e < e_bins; e++) { + for (int64_t m = 0; m < mesh_bins; m++) { + // Place particle, energy, and mesh indices into their storage positions + std::array idx = {0, 0, 0}; + idx[transpose[0]] = particle_idx; + idx[transpose[1]] = e; + idx[transpose[2]] = static_cast(m); + + // Compute flat filter combination index (row-major over filter dims) + int flat = idx[0] * stride0 + idx[1] * stride1 + idx[2]; + + sum(e, m) = results(flat, score_index, i_sum); + sum_sq(e, m) = results(flat, score_index, i_sum_sq); + } + } int n = tally->n_realizations_; ////////////////////////////////////////////// @@ -1155,7 +1178,8 @@ extern "C" int openmc_weight_windows_set_bounds(int32_t index, return err; const auto& wws = variance_reduction::weight_windows[index]; - wws->set_bounds({lower_bounds, size}, {upper_bounds, size}); + wws->set_bounds(span(lower_bounds, size), + span(upper_bounds, size)); return 0; } diff --git a/src/wmp.cpp b/src/wmp.cpp index e9c4fb5e3e..6f72e1ba4c 100644 --- a/src/wmp.cpp +++ b/src/wmp.cpp @@ -33,22 +33,22 @@ WindowedMultipole::WindowedMultipole(hid_t group) // Read the "data" array. Use its shape to figure out the number of poles // and residue types in this data. read_dataset(group, "data", data_); - int n_residues = data_.shape()[1] - 1; + int n_residues = data_.shape(1) - 1; // Check to see if this data includes fission residues. fissionable_ = (n_residues == 3); // Read the "windows" array and use its shape to figure out the number of // windows. - xt::xtensor windows; + tensor::Tensor windows; read_dataset(group, "windows", windows); - int n_windows = windows.shape()[0]; + int n_windows = windows.shape(0); windows -= 1; // Adjust to 0-based indices // Read the "broaden_poly" arrays. - xt::xtensor broaden_poly; + tensor::Tensor broaden_poly; read_dataset(group, "broaden_poly", broaden_poly); - if (n_windows != broaden_poly.shape()[0]) { + if (n_windows != broaden_poly.shape(0)) { fatal_error("broaden_poly array shape is not consistent with the windows " "array shape in WMP library for " + name_ + "."); @@ -56,12 +56,12 @@ WindowedMultipole::WindowedMultipole(hid_t group) // Read the "curvefit" array. read_dataset(group, "curvefit", curvefit_); - if (n_windows != curvefit_.shape()[0]) { + if (n_windows != curvefit_.shape(0)) { fatal_error("curvefit array shape is not consistent with the windows " "array shape in WMP library for " + name_ + "."); } - fit_order_ = curvefit_.shape()[1] - 1; + fit_order_ = curvefit_.shape(1) - 1; // Check the code is compiling to work with sufficiently high fit order if (fit_order_ + 1 > MAX_POLY_COEFFICIENTS) { diff --git a/src/xsdata.cpp b/src/xsdata.cpp index 1929f51e6f..33d063a7b9 100644 --- a/src/xsdata.cpp +++ b/src/xsdata.cpp @@ -5,10 +5,7 @@ #include #include -#include "xtensor/xbuilder.hpp" -#include "xtensor/xindex_view.hpp" -#include "xtensor/xmath.hpp" -#include "xtensor/xview.hpp" +#include "openmc/tensor.h" #include "openmc/constants.h" #include "openmc/error.h" @@ -37,32 +34,32 @@ XsData::XsData(bool fissionable, AngleDistributionType scatter_format, } // allocate all [temperature][angle][in group] quantities vector shape {n_ang, n_g_}; - total = xt::zeros(shape); - absorption = xt::zeros(shape); - inverse_velocity = xt::zeros(shape); + total = tensor::zeros(shape); + absorption = tensor::zeros(shape); + inverse_velocity = tensor::zeros(shape); if (fissionable) { - fission = xt::zeros(shape); - nu_fission = xt::zeros(shape); - prompt_nu_fission = xt::zeros(shape); - kappa_fission = xt::zeros(shape); + fission = tensor::zeros(shape); + nu_fission = tensor::zeros(shape); + prompt_nu_fission = tensor::zeros(shape); + kappa_fission = tensor::zeros(shape); } // allocate decay_rate; [temperature][angle][delayed group] shape[1] = n_dg_; - decay_rate = xt::zeros(shape); + decay_rate = tensor::zeros(shape); if (fissionable) { shape = {n_ang, n_dg_, n_g_}; // allocate delayed_nu_fission; [temperature][angle][delay group][in group] - delayed_nu_fission = xt::zeros(shape); + delayed_nu_fission = tensor::zeros(shape); // chi_prompt; [temperature][angle][in group][out group] shape = {n_ang, n_g_, n_g_}; - chi_prompt = xt::zeros(shape); + chi_prompt = tensor::zeros(shape); // chi_delayed; [temperature][angle][delay group][in group][out group] shape = {n_ang, n_dg_, n_g_, n_g_}; - chi_delayed = xt::zeros(shape); + chi_delayed = tensor::zeros(shape); } for (int a = 0; a < n_ang; a++) { @@ -85,28 +82,30 @@ void XsData::from_hdf5(hid_t xsdata_grp, bool fissionable, { // Reconstruct the dimension information so it doesn't need to be passed size_t n_ang = n_pol * n_azi; - size_t energy_groups = total.shape()[1]; + size_t energy_groups = total.shape(1); // Set the fissionable-specific data if (fissionable) { fission_from_hdf5(xsdata_grp, n_ang, is_isotropic); } // Get the non-fission-specific data - read_nd_vector(xsdata_grp, "decay-rate", decay_rate); - read_nd_vector(xsdata_grp, "absorption", absorption, true); - read_nd_vector(xsdata_grp, "inverse-velocity", inverse_velocity); + read_nd_tensor(xsdata_grp, "decay-rate", decay_rate); + read_nd_tensor(xsdata_grp, "absorption", absorption, true); + read_nd_tensor(xsdata_grp, "inverse-velocity", inverse_velocity); // Get scattering data scatter_from_hdf5( xsdata_grp, n_ang, scatter_format, final_scatter_format, order_data); - // Check absorption to ensure it is not 0 since it is often the - // denominator in tally methods - xt::filtration(absorption, xt::equal(absorption, 0.)) = 1.e-10; + // Replace zero absorption values with a small number to avoid + // division by zero in tally methods + for (size_t i = 0; i < absorption.size(); i++) + if (absorption.data()[i] == 0.0) + absorption.data()[i] = 1.e-10; // Get or calculate the total x/s if (object_exists(xsdata_grp, "total")) { - read_nd_vector(xsdata_grp, "total", total); + read_nd_tensor(xsdata_grp, "total", total); } else { for (size_t a = 0; a < n_ang; a++) { for (size_t gin = 0; gin < energy_groups; gin++) { @@ -115,8 +114,11 @@ void XsData::from_hdf5(hid_t xsdata_grp, bool fissionable, } } - // Fix if total is 0, since it is in the denominator when tallying - xt::filtration(total, xt::equal(total, 0.)) = 1.e-10; + // Replace zero total cross sections with a small number to avoid + // division by zero in tally methods + for (size_t i = 0; i < total.size(); i++) + if (total.data()[i] == 0.0) + total.data()[i] = 1.e-10; } //============================================================================== @@ -127,21 +129,30 @@ void XsData::fission_vector_beta_from_hdf5( // Data is provided as nu-fission and chi with a beta for delayed info // Get chi - xt::xtensor temp_chi({n_ang, n_g_}, 0.); - read_nd_vector(xsdata_grp, "chi", temp_chi, true); + tensor::Tensor temp_chi = tensor::zeros({n_ang, n_g_}); + read_nd_tensor(xsdata_grp, "chi", temp_chi, true); - // Normalize chi by summing over the outgoing groups for each incoming angle - temp_chi /= xt::view(xt::sum(temp_chi, {1}), xt::all(), xt::newaxis()); + // Normalize chi so it sums to 1 over outgoing groups for each angle + for (size_t a = 0; a < n_ang; a++) { + tensor::View row = temp_chi.slice(a); + row /= row.sum(); + } - // Now every incoming group in prompt_chi and delayed_chi is the normalized - // chi we just made - chi_prompt = xt::view(temp_chi, xt::all(), xt::newaxis(), xt::all()); - chi_delayed = - xt::view(temp_chi, xt::all(), xt::newaxis(), xt::newaxis(), xt::all()); + // Replicate the energy spectrum across all incoming groups — the + // spectrum is independent of the incoming neutron energy + for (size_t a = 0; a < n_ang; a++) + for (size_t gin = 0; gin < n_g_; gin++) + chi_prompt.slice(a, gin) = temp_chi.slice(a); + + // Same spectrum for delayed neutrons, replicated across delayed groups + for (size_t a = 0; a < n_ang; a++) + for (size_t d = 0; d < n_dg_; d++) + for (size_t gin = 0; gin < n_g_; gin++) + chi_delayed.slice(a, d, gin) = temp_chi.slice(a); // Get nu-fission - xt::xtensor temp_nufiss({n_ang, n_g_}, 0.); - read_nd_vector(xsdata_grp, "nu-fission", temp_nufiss, true); + tensor::Tensor temp_nufiss = tensor::zeros({n_ang, n_g_}); + read_nd_tensor(xsdata_grp, "nu-fission", temp_nufiss, true); // Get beta (strategy will depend upon the number of dimensions in beta) hid_t beta_dset = open_dataset(xsdata_grp, "beta"); @@ -151,26 +162,39 @@ void XsData::fission_vector_beta_from_hdf5( if (!is_isotropic) ndim_target += 2; if (beta_ndims == ndim_target) { - xt::xtensor temp_beta({n_ang, n_dg_}, 0.); - read_nd_vector(xsdata_grp, "beta", temp_beta, true); + tensor::Tensor temp_beta = tensor::zeros({n_ang, n_dg_}); + read_nd_tensor(xsdata_grp, "beta", temp_beta, true); - // Set prompt_nu_fission = (1. - beta_total)*nu_fission - prompt_nu_fission = temp_nufiss * (1. - xt::sum(temp_beta, {1})); + // prompt_nu_fission = (1 - sum_of_beta) * nu_fission + auto beta_sum = temp_beta.sum(1); + for (size_t a = 0; a < n_ang; a++) + for (size_t g = 0; g < n_g_; g++) + prompt_nu_fission(a, g) = temp_nufiss(a, g) * (1.0 - beta_sum(a)); - // Set delayed_nu_fission as beta * nu_fission - delayed_nu_fission = - xt::view(temp_beta, xt::all(), xt::all(), xt::newaxis()) * - xt::view(temp_nufiss, xt::all(), xt::newaxis(), xt::all()); + // Delayed nu-fission is the outer product of the delayed neutron + // fraction (beta) and the fission production rate (nu-fission) + for (size_t a = 0; a < n_ang; a++) + for (size_t d = 0; d < n_dg_; d++) + for (size_t g = 0; g < n_g_; g++) + delayed_nu_fission(a, d, g) = temp_beta(a, d) * temp_nufiss(a, g); } else if (beta_ndims == ndim_target + 1) { - xt::xtensor temp_beta({n_ang, n_dg_, n_g_}, 0.); - read_nd_vector(xsdata_grp, "beta", temp_beta, true); + tensor::Tensor temp_beta = + tensor::zeros({n_ang, n_dg_, n_g_}); + read_nd_tensor(xsdata_grp, "beta", temp_beta, true); - // Set prompt_nu_fission = (1. - beta_total)*nu_fission - prompt_nu_fission = temp_nufiss * (1. - xt::sum(temp_beta, {1})); + // prompt_nu_fission = (1 - sum_of_beta) * nu_fission + // Here beta is energy-dependent, so sum over delayed groups (axis 1) + auto beta_sum = temp_beta.sum(1); + for (size_t a = 0; a < n_ang; a++) + for (size_t g = 0; g < n_g_; g++) + prompt_nu_fission(a, g) = temp_nufiss(a, g) * (1.0 - beta_sum(a, g)); - // Set delayed_nu_fission as beta * nu_fission - delayed_nu_fission = - temp_beta * xt::view(temp_nufiss, xt::all(), xt::newaxis(), xt::all()); + // Delayed nu-fission: beta is already energy-dependent [n_ang, n_dg, n_g], + // so scale each delayed group's beta by the total nu-fission for that group + for (size_t a = 0; a < n_ang; a++) + for (size_t d = 0; d < n_dg_; d++) + for (size_t g = 0; g < n_g_; g++) + delayed_nu_fission(a, d, g) = temp_beta(a, d, g) * temp_nufiss(a, g); } } @@ -179,29 +203,42 @@ void XsData::fission_vector_no_beta_from_hdf5(hid_t xsdata_grp, size_t n_ang) // Data is provided separately as prompt + delayed nu-fission and chi // Get chi-prompt - xt::xtensor temp_chi_p({n_ang, n_g_}, 0.); - read_nd_vector(xsdata_grp, "chi-prompt", temp_chi_p, true); + tensor::Tensor temp_chi_p = tensor::zeros({n_ang, n_g_}); + read_nd_tensor(xsdata_grp, "chi-prompt", temp_chi_p, true); - // Normalize chi by summing over the outgoing groups for each incoming angle - temp_chi_p /= xt::view(xt::sum(temp_chi_p, {1}), xt::all(), xt::newaxis()); + // Normalize prompt chi so it sums to 1 over outgoing groups for each angle + for (size_t a = 0; a < n_ang; a++) { + tensor::View row = temp_chi_p.slice(a); + row /= row.sum(); + } // Get chi-delayed - xt::xtensor temp_chi_d({n_ang, n_dg_, n_g_}, 0.); - read_nd_vector(xsdata_grp, "chi-delayed", temp_chi_d, true); + tensor::Tensor temp_chi_d = + tensor::zeros({n_ang, n_dg_, n_g_}); + read_nd_tensor(xsdata_grp, "chi-delayed", temp_chi_d, true); - // Normalize chi by summing over the outgoing groups for each incoming angle - temp_chi_d /= - xt::view(xt::sum(temp_chi_d, {2}), xt::all(), xt::all(), xt::newaxis()); + // Normalize delayed chi so it sums to 1 over outgoing groups for each + // angle and delayed group + for (size_t a = 0; a < n_ang; a++) + for (size_t d = 0; d < n_dg_; d++) { + tensor::View row = temp_chi_d.slice(a, d); + row /= row.sum(); + } - // Now assign the prompt and delayed chis by replicating for each incoming - // group - chi_prompt = xt::view(temp_chi_p, xt::all(), xt::newaxis(), xt::all()); - chi_delayed = - xt::view(temp_chi_d, xt::all(), xt::all(), xt::newaxis(), xt::all()); + // Replicate the prompt spectrum across all incoming groups + for (size_t a = 0; a < n_ang; a++) + for (size_t gin = 0; gin < n_g_; gin++) + chi_prompt.slice(a, gin) = temp_chi_p.slice(a); + + // Replicate the delayed spectrum across all incoming groups + for (size_t a = 0; a < n_ang; a++) + for (size_t d = 0; d < n_dg_; d++) + for (size_t gin = 0; gin < n_g_; gin++) + chi_delayed.slice(a, d, gin) = temp_chi_d.slice(a, d); // Get prompt and delayed nu-fission directly - read_nd_vector(xsdata_grp, "prompt-nu-fission", prompt_nu_fission, true); - read_nd_vector(xsdata_grp, "delayed-nu-fission", delayed_nu_fission, true); + read_nd_tensor(xsdata_grp, "prompt-nu-fission", prompt_nu_fission, true); + read_nd_tensor(xsdata_grp, "delayed-nu-fission", delayed_nu_fission, true); } void XsData::fission_vector_no_delayed_from_hdf5(hid_t xsdata_grp, size_t n_ang) @@ -210,17 +247,22 @@ void XsData::fission_vector_no_delayed_from_hdf5(hid_t xsdata_grp, size_t n_ang) // Therefore, the code only considers the data as prompt. // Get chi - xt::xtensor temp_chi({n_ang, n_g_}, 0.); - read_nd_vector(xsdata_grp, "chi", temp_chi, true); + tensor::Tensor temp_chi = tensor::zeros({n_ang, n_g_}); + read_nd_tensor(xsdata_grp, "chi", temp_chi, true); - // Normalize chi by summing over the outgoing groups for each incoming angle - temp_chi /= xt::view(xt::sum(temp_chi, {1}), xt::all(), xt::newaxis()); + // Normalize chi so it sums to 1 over outgoing groups for each angle + for (size_t a = 0; a < n_ang; a++) { + tensor::View row = temp_chi.slice(a); + row /= row.sum(); + } - // Now every incoming group in self.chi is the normalized chi we just made - chi_prompt = xt::view(temp_chi, xt::all(), xt::newaxis(), xt::all()); + // Replicate the energy spectrum across all incoming groups + for (size_t a = 0; a < n_ang; a++) + for (size_t gin = 0; gin < n_g_; gin++) + chi_prompt.slice(a, gin) = temp_chi.slice(a); // Get nu-fission directly - read_nd_vector(xsdata_grp, "nu-fission", prompt_nu_fission, true); + read_nd_tensor(xsdata_grp, "nu-fission", prompt_nu_fission, true); } //============================================================================== @@ -231,8 +273,9 @@ void XsData::fission_matrix_beta_from_hdf5( // Data is provided as nu-fission and chi with a beta for delayed info // Get nu-fission matrix - xt::xtensor temp_matrix({n_ang, n_g_, n_g_}, 0.); - read_nd_vector(xsdata_grp, "nu-fission", temp_matrix, true); + tensor::Tensor temp_matrix = + tensor::zeros({n_ang, n_g_, n_g_}); + read_nd_tensor(xsdata_grp, "nu-fission", temp_matrix, true); // Get beta (strategy will depend upon the number of dimensions in beta) hid_t beta_dset = open_dataset(xsdata_grp, "beta"); @@ -242,65 +285,92 @@ void XsData::fission_matrix_beta_from_hdf5( if (!is_isotropic) ndim_target += 2; if (beta_ndims == ndim_target) { - xt::xtensor temp_beta({n_ang, n_dg_}, 0.); - read_nd_vector(xsdata_grp, "beta", temp_beta, true); + tensor::Tensor temp_beta = tensor::zeros({n_ang, n_dg_}); + read_nd_tensor(xsdata_grp, "beta", temp_beta, true); - xt::xtensor temp_beta_sum({n_ang}, 0.); - temp_beta_sum = xt::sum(temp_beta, {1}); + auto beta_sum = temp_beta.sum(1); + auto matrix_gout_sum = temp_matrix.sum(2); - // prompt_nu_fission is the sum of this matrix over outgoing groups and - // multiplied by (1 - beta_sum) - prompt_nu_fission = xt::sum(temp_matrix, {2}) * (1. - temp_beta_sum); + // prompt_nu_fission = sum_gout(matrix) * (1 - beta_total) + for (size_t a = 0; a < n_ang; a++) + for (size_t g = 0; g < n_g_; g++) + prompt_nu_fission(a, g) = matrix_gout_sum(a, g) * (1.0 - beta_sum(a)); - // Store chi-prompt - chi_prompt = - xt::view(1.0 - temp_beta_sum, xt::all(), xt::newaxis(), xt::newaxis()) * - temp_matrix; + // chi_prompt = (1 - beta_total) * nu-fission matrix (unnormalized) + for (size_t a = 0; a < n_ang; a++) + for (size_t gin = 0; gin < n_g_; gin++) + for (size_t gout = 0; gout < n_g_; gout++) + chi_prompt(a, gin, gout) = + (1.0 - beta_sum(a)) * temp_matrix(a, gin, gout); - // delayed_nu_fission is the sum of this matrix over outgoing groups and - // multiplied by beta - delayed_nu_fission = - xt::view(temp_beta, xt::all(), xt::all(), xt::newaxis()) * - xt::view(xt::sum(temp_matrix, {2}), xt::all(), xt::newaxis(), xt::all()); + // Delayed nu-fission is the outer product of the delayed neutron + // fraction (beta) and the total fission rate summed over outgoing groups + for (size_t a = 0; a < n_ang; a++) + for (size_t d = 0; d < n_dg_; d++) + for (size_t g = 0; g < n_g_; g++) + delayed_nu_fission(a, d, g) = temp_beta(a, d) * matrix_gout_sum(a, g); - // Store chi-delayed - chi_delayed = - xt::view(temp_beta, xt::all(), xt::all(), xt::newaxis(), xt::newaxis()) * - xt::view(temp_matrix, xt::all(), xt::newaxis(), xt::all(), xt::all()); + // chi_delayed = beta * nu-fission matrix, expanded across delayed groups + for (size_t a = 0; a < n_ang; a++) + for (size_t d = 0; d < n_dg_; d++) + for (size_t gin = 0; gin < n_g_; gin++) + for (size_t gout = 0; gout < n_g_; gout++) + chi_delayed(a, d, gin, gout) = + temp_beta(a, d) * temp_matrix(a, gin, gout); } else if (beta_ndims == ndim_target + 1) { - xt::xtensor temp_beta({n_ang, n_dg_, n_g_}, 0.); - read_nd_vector(xsdata_grp, "beta", temp_beta, true); + tensor::Tensor temp_beta = + tensor::zeros({n_ang, n_dg_, n_g_}); + read_nd_tensor(xsdata_grp, "beta", temp_beta, true); - xt::xtensor temp_beta_sum({n_ang, n_g_}, 0.); - temp_beta_sum = xt::sum(temp_beta, {1}); + auto beta_sum = temp_beta.sum(1); + auto matrix_gout_sum = temp_matrix.sum(2); - // prompt_nu_fission is the sum of this matrix over outgoing groups and - // multiplied by (1 - beta_sum) - prompt_nu_fission = xt::sum(temp_matrix, {2}) * (1. - temp_beta_sum); + // prompt_nu_fission = sum_gout(matrix) * (1 - beta_total) + // Here beta is energy-dependent, so beta_sum is 2D [n_ang, n_g] + for (size_t a = 0; a < n_ang; a++) + for (size_t g = 0; g < n_g_; g++) + prompt_nu_fission(a, g) = + matrix_gout_sum(a, g) * (1.0 - beta_sum(a, g)); - // Store chi-prompt - chi_prompt = - xt::view(1.0 - temp_beta_sum, xt::all(), xt::all(), xt::newaxis()) * - temp_matrix; + // chi_prompt = (1 - beta_sum) * nu-fission matrix (unnormalized) + for (size_t a = 0; a < n_ang; a++) + for (size_t gin = 0; gin < n_g_; gin++) + for (size_t gout = 0; gout < n_g_; gout++) + chi_prompt(a, gin, gout) = + (1.0 - beta_sum(a, gin)) * temp_matrix(a, gin, gout); - // delayed_nu_fission is the sum of this matrix over outgoing groups and - // multiplied by beta - delayed_nu_fission = temp_beta * xt::view(xt::sum(temp_matrix, {2}), - xt::all(), xt::newaxis(), xt::all()); + // Delayed nu-fission: beta is energy-dependent [n_ang, n_dg, n_g], + // scale by total fission rate summed over outgoing groups + for (size_t a = 0; a < n_ang; a++) + for (size_t d = 0; d < n_dg_; d++) + for (size_t g = 0; g < n_g_; g++) + delayed_nu_fission(a, d, g) = + temp_beta(a, d, g) * matrix_gout_sum(a, g); - // Store chi-delayed - chi_delayed = - xt::view(temp_beta, xt::all(), xt::all(), xt::all(), xt::newaxis()) * - xt::view(temp_matrix, xt::all(), xt::newaxis(), xt::all(), xt::all()); + // chi_delayed = beta * nu-fission matrix, expanded across delayed groups + for (size_t a = 0; a < n_ang; a++) + for (size_t d = 0; d < n_dg_; d++) + for (size_t gin = 0; gin < n_g_; gin++) + for (size_t gout = 0; gout < n_g_; gout++) + chi_delayed(a, d, gin, gout) = + temp_beta(a, d, gin) * temp_matrix(a, gin, gout); } - // Normalize both chis - chi_prompt /= - xt::view(xt::sum(chi_prompt, {2}), xt::all(), xt::all(), xt::newaxis()); + // Normalize chi_prompt so it sums to 1 over outgoing groups + for (size_t a = 0; a < n_ang; a++) + for (size_t gin = 0; gin < n_g_; gin++) { + tensor::View row = chi_prompt.slice(a, gin); + row /= row.sum(); + } - chi_delayed /= xt::view( - xt::sum(chi_delayed, {3}), xt::all(), xt::all(), xt::all(), xt::newaxis()); + // Normalize chi_delayed so it sums to 1 over outgoing groups + for (size_t a = 0; a < n_ang; a++) + for (size_t d = 0; d < n_dg_; d++) + for (size_t gin = 0; gin < n_g_; gin++) { + tensor::View row = chi_delayed.slice(a, d, gin); + row /= row.sum(); + } } void XsData::fission_matrix_no_beta_from_hdf5(hid_t xsdata_grp, size_t n_ang) @@ -308,28 +378,36 @@ void XsData::fission_matrix_no_beta_from_hdf5(hid_t xsdata_grp, size_t n_ang) // Data is provided separately as prompt + delayed nu-fission and chi // Get the prompt nu-fission matrix - xt::xtensor temp_matrix_p({n_ang, n_g_, n_g_}, 0.); - read_nd_vector(xsdata_grp, "prompt-nu-fission", temp_matrix_p, true); + tensor::Tensor temp_matrix_p = + tensor::zeros({n_ang, n_g_, n_g_}); + read_nd_tensor(xsdata_grp, "prompt-nu-fission", temp_matrix_p, true); // prompt_nu_fission is the sum over outgoing groups - prompt_nu_fission = xt::sum(temp_matrix_p, {2}); + prompt_nu_fission = temp_matrix_p.sum(2); - // chi_prompt is this matrix but normalized over outgoing groups, which we - // have already stored in prompt_nu_fission - chi_prompt = temp_matrix_p / - xt::view(prompt_nu_fission, xt::all(), xt::all(), xt::newaxis()); + // chi_prompt is the nu-fission matrix normalized over outgoing groups + for (size_t a = 0; a < n_ang; a++) + for (size_t gin = 0; gin < n_g_; gin++) + for (size_t gout = 0; gout < n_g_; gout++) + chi_prompt(a, gin, gout) = + temp_matrix_p(a, gin, gout) / prompt_nu_fission(a, gin); // Get the delayed nu-fission matrix - xt::xtensor temp_matrix_d({n_ang, n_dg_, n_g_, n_g_}, 0.); - read_nd_vector(xsdata_grp, "delayed-nu-fission", temp_matrix_d, true); + tensor::Tensor temp_matrix_d = + tensor::zeros({n_ang, n_dg_, n_g_, n_g_}); + read_nd_tensor(xsdata_grp, "delayed-nu-fission", temp_matrix_d, true); // delayed_nu_fission is the sum over outgoing groups - delayed_nu_fission = xt::sum(temp_matrix_d, {3}); + delayed_nu_fission = temp_matrix_d.sum(3); - // chi_prompt is this matrix but normalized over outgoing groups, which we - // have already stored in prompt_nu_fission - chi_delayed = temp_matrix_d / xt::view(delayed_nu_fission, xt::all(), - xt::all(), xt::all(), xt::newaxis()); + // chi_delayed is the delayed nu-fission matrix normalized over outgoing + // groups + for (size_t a = 0; a < n_ang; a++) + for (size_t d = 0; d < n_dg_; d++) + for (size_t gin = 0; gin < n_g_; gin++) + for (size_t gout = 0; gout < n_g_; gout++) + chi_delayed(a, d, gin, gout) = + temp_matrix_d(a, d, gin, gout) / delayed_nu_fission(a, d, gin); } void XsData::fission_matrix_no_delayed_from_hdf5(hid_t xsdata_grp, size_t n_ang) @@ -338,16 +416,19 @@ void XsData::fission_matrix_no_delayed_from_hdf5(hid_t xsdata_grp, size_t n_ang) // Therefore, the code only considers the data as prompt. // Get nu-fission matrix - xt::xtensor temp_matrix({n_ang, n_g_, n_g_}, 0.); - read_nd_vector(xsdata_grp, "nu-fission", temp_matrix, true); + tensor::Tensor temp_matrix = + tensor::zeros({n_ang, n_g_, n_g_}); + read_nd_tensor(xsdata_grp, "nu-fission", temp_matrix, true); // prompt_nu_fission is the sum over outgoing groups - prompt_nu_fission = xt::sum(temp_matrix, {2}); + prompt_nu_fission = temp_matrix.sum(2); - // chi_prompt is this matrix but normalized over outgoing groups, which we - // have already stored in prompt_nu_fission - chi_prompt = temp_matrix / - xt::view(prompt_nu_fission, xt::all(), xt::all(), xt::newaxis()); + // chi_prompt is the nu-fission matrix normalized over outgoing groups + for (size_t a = 0; a < n_ang; a++) + for (size_t gin = 0; gin < n_g_; gin++) + for (size_t gout = 0; gout < n_g_; gout++) + chi_prompt(a, gin, gout) = + temp_matrix(a, gin, gout) / prompt_nu_fission(a, gin); } //============================================================================== @@ -356,8 +437,8 @@ void XsData::fission_from_hdf5( hid_t xsdata_grp, size_t n_ang, bool is_isotropic) { // Get the fission and kappa_fission data xs; these are optional - read_nd_vector(xsdata_grp, "fission", fission); - read_nd_vector(xsdata_grp, "kappa-fission", kappa_fission); + read_nd_tensor(xsdata_grp, "fission", fission); + read_nd_tensor(xsdata_grp, "kappa-fission", kappa_fission); // Get the data; the strategy for doing so depends on if the data is provided // as a nu-fission matrix or a set of chi and nu-fission vectors @@ -388,7 +469,7 @@ void XsData::fission_from_hdf5( if (n_dg_ == 0) { nu_fission = prompt_nu_fission; } else { - nu_fission = prompt_nu_fission + xt::sum(delayed_nu_fission, {1}); + nu_fission = prompt_nu_fission + delayed_nu_fission.sum(1); } } @@ -404,10 +485,10 @@ void XsData::scatter_from_hdf5(hid_t xsdata_grp, size_t n_ang, hid_t scatt_grp = open_group(xsdata_grp, "scatter_data"); // Get the outgoing group boundary indices - xt::xtensor gmin({n_ang, n_g_}, 0.); - read_nd_vector(scatt_grp, "g_min", gmin, true); - xt::xtensor gmax({n_ang, n_g_}, 0.); - read_nd_vector(scatt_grp, "g_max", gmax, true); + tensor::Tensor gmin = tensor::zeros({n_ang, n_g_}); + read_nd_tensor(scatt_grp, "g_min", gmin, true); + tensor::Tensor gmax = tensor::zeros({n_ang, n_g_}); + read_nd_tensor(scatt_grp, "g_max", gmax, true); // Make gmin and gmax start from 0 vice 1 as they do in the library gmin -= 1; @@ -415,11 +496,11 @@ void XsData::scatter_from_hdf5(hid_t xsdata_grp, size_t n_ang, // Now use this info to find the length of a vector to hold the flattened // data. - size_t length = order_data * xt::sum(gmax - gmin + 1)(); + size_t length = order_data * (gmax - gmin + 1).sum(); double_4dvec input_scatt(n_ang, double_3dvec(n_g_)); - xt::xtensor temp_arr({length}, 0.); - read_nd_vector(scatt_grp, "scatter_matrix", temp_arr, true); + tensor::Tensor temp_arr = tensor::zeros({length}); + read_nd_tensor(scatt_grp, "scatter_matrix", temp_arr, true); // Compare the number of orders given with the max order of the problem; // strip off the superfluous orders if needed @@ -451,7 +532,7 @@ void XsData::scatter_from_hdf5(hid_t xsdata_grp, size_t n_ang, double_3dvec temp_mult(n_ang, double_2dvec(n_g_)); if (object_exists(scatt_grp, "multiplicity_matrix")) { temp_arr.resize({length / order_data}); - read_nd_vector(scatt_grp, "multiplicity_matrix", temp_arr); + read_nd_tensor(scatt_grp, "multiplicity_matrix", temp_arr); // convert the flat temp_arr to a jagged array for passing to scatt data size_t temp_idx = 0; @@ -481,8 +562,8 @@ void XsData::scatter_from_hdf5(hid_t xsdata_grp, size_t n_ang, final_scatter_format == AngleDistributionType::TABULAR) { for (size_t a = 0; a < n_ang; a++) { ScattDataLegendre legendre_scatt; - xt::xtensor in_gmin = xt::view(gmin, a, xt::all()); - xt::xtensor in_gmax = xt::view(gmax, a, xt::all()); + tensor::Tensor in_gmin(gmin.slice(a)); + tensor::Tensor in_gmax(gmax.slice(a)); legendre_scatt.init(in_gmin, in_gmax, temp_mult[a], input_scatt[a]); @@ -496,8 +577,8 @@ void XsData::scatter_from_hdf5(hid_t xsdata_grp, size_t n_ang, // We are sticking with the current representation // Initialize the ScattData object with this data for (size_t a = 0; a < n_ang; a++) { - xt::xtensor in_gmin = xt::view(gmin, a, xt::all()); - xt::xtensor in_gmax = xt::view(gmax, a, xt::all()); + tensor::Tensor in_gmin(gmin.slice(a)); + tensor::Tensor in_gmax(gmax.slice(a)); scatter[a]->init(in_gmin, in_gmax, temp_mult[a], input_scatt[a]); } } @@ -519,33 +600,67 @@ void XsData::combine( if (i == 0) { inverse_velocity = that->inverse_velocity; } - if (that->prompt_nu_fission.shape()[0] > 0) { + if (!that->prompt_nu_fission.empty()) { nu_fission += scalar * that->nu_fission; prompt_nu_fission += scalar * that->prompt_nu_fission; kappa_fission += scalar * that->kappa_fission; fission += scalar * that->fission; delayed_nu_fission += scalar * that->delayed_nu_fission; - chi_prompt += scalar * - xt::view(xt::sum(that->prompt_nu_fission, {1}), xt::all(), - xt::newaxis(), xt::newaxis()) * - that->chi_prompt; - chi_delayed += scalar * - xt::view(xt::sum(that->delayed_nu_fission, {2}), xt::all(), - xt::all(), xt::newaxis(), xt::newaxis()) * - that->chi_delayed; + // Accumulate chi_prompt weighted by total prompt nu-fission + // (summed over energy groups) for this constituent + { + auto pnf_sum = that->prompt_nu_fission.sum(1); + size_t n_ang = chi_prompt.shape(0); + size_t n_g = chi_prompt.shape(1); + for (size_t a = 0; a < n_ang; a++) + for (size_t gin = 0; gin < n_g; gin++) + for (size_t gout = 0; gout < n_g; gout++) + chi_prompt(a, gin, gout) += + scalar * pnf_sum(a) * that->chi_prompt(a, gin, gout); + } + // Accumulate chi_delayed weighted by total delayed nu-fission + // (summed over energy groups) for this constituent + { + auto dnf_sum = that->delayed_nu_fission.sum(2); + size_t n_ang = chi_delayed.shape(0); + size_t n_dg = chi_delayed.shape(1); + size_t n_g = chi_delayed.shape(2); + for (size_t a = 0; a < n_ang; a++) + for (size_t d = 0; d < n_dg; d++) + for (size_t gin = 0; gin < n_g; gin++) + for (size_t gout = 0; gout < n_g; gout++) + chi_delayed(a, d, gin, gout) += + scalar * dnf_sum(a, d) * that->chi_delayed(a, d, gin, gout); + } } decay_rate += scalar * that->decay_rate; } - // Ensure the chi_prompt and chi_delayed are normalized to 1 for each - // azimuthal angle and delayed group (for chi_delayed) - chi_prompt /= - xt::view(xt::sum(chi_prompt, {2}), xt::all(), xt::all(), xt::newaxis()); - chi_delayed /= xt::view( - xt::sum(chi_delayed, {3}), xt::all(), xt::all(), xt::all(), xt::newaxis()); + // Normalize chi_prompt so it sums to 1 over outgoing groups + { + size_t n_ang = chi_prompt.shape(0); + size_t n_g = chi_prompt.shape(1); + for (size_t a = 0; a < n_ang; a++) + for (size_t gin = 0; gin < n_g; gin++) { + tensor::View row = chi_prompt.slice(a, gin); + row /= row.sum(); + } + } + // Normalize chi_delayed so it sums to 1 over outgoing groups + { + size_t n_ang = chi_delayed.shape(0); + size_t n_dg = chi_delayed.shape(1); + size_t n_g = chi_delayed.shape(2); + for (size_t a = 0; a < n_ang; a++) + for (size_t d = 0; d < n_dg; d++) + for (size_t gin = 0; gin < n_g; gin++) { + tensor::View row = chi_delayed.slice(a, d, gin); + row /= row.sum(); + } + } // Allow the ScattData object to combine itself - for (size_t a = 0; a < total.shape()[0]; a++) { + for (size_t a = 0; a < total.shape(0); a++) { // Build vector of the scattering objects to incorporate vector those_scatts(those_xs.size()); for (size_t i = 0; i < those_xs.size(); i++) { diff --git a/tests/cpp_unit_tests/CMakeLists.txt b/tests/cpp_unit_tests/CMakeLists.txt index 20341b420f..ce7e539ea5 100644 --- a/tests/cpp_unit_tests/CMakeLists.txt +++ b/tests/cpp_unit_tests/CMakeLists.txt @@ -7,6 +7,7 @@ set(TEST_NAMES test_mcpl_stat_sum test_mesh test_region + test_tensor # Add additional unit test files here ) diff --git a/tests/cpp_unit_tests/test_tensor.cpp b/tests/cpp_unit_tests/test_tensor.cpp new file mode 100644 index 0000000000..6eae1cea6a --- /dev/null +++ b/tests/cpp_unit_tests/test_tensor.cpp @@ -0,0 +1,987 @@ +#include +#include + +#include +#include + +#include "openmc/tensor.h" + +using namespace openmc; +using namespace openmc::tensor; + +// ============================================================================ +// Tensor constructors +// ============================================================================ + +TEST_CASE("Tensor default constructor") +{ + Tensor t; + REQUIRE(t.size() == 0); + REQUIRE(t.empty()); + REQUIRE(t.shape().empty()); +} + +TEST_CASE("Tensor shape constructor") +{ + Tensor t1({5}); + REQUIRE(t1.size() == 5); + REQUIRE(t1.shape().size() == 1); + REQUIRE(t1.shape(0) == 5); + + Tensor t2({3, 4}); + REQUIRE(t2.size() == 12); + REQUIRE(t2.shape().size() == 2); + REQUIRE(t2.shape(0) == 3); + REQUIRE(t2.shape(1) == 4); + + Tensor t3({2, 3, 4}); + REQUIRE(t3.size() == 24); + REQUIRE(t3.shape().size() == 3); +} + +TEST_CASE("Tensor shape + fill constructor") +{ + Tensor t({2, 3}, 7.0); + REQUIRE(t.size() == 6); + for (size_t i = 0; i < t.size(); ++i) + REQUIRE(t[i] == 7.0); +} + +TEST_CASE("Tensor pointer constructor") +{ + double vals[] = {1.0, 2.0, 3.0, 4.0}; + Tensor t(vals, 4); + REQUIRE(t.size() == 4); + REQUIRE(t.shape(0) == 4); + REQUIRE(t[0] == 1.0); + REQUIRE(t[1] == 2.0); + REQUIRE(t[2] == 3.0); + REQUIRE(t[3] == 4.0); +} + +TEST_CASE("Tensor copy and move") +{ + Tensor a({2, 3}, 5.0); + Tensor b(a); + REQUIRE(b.size() == 6); + REQUIRE(b(0, 0) == 5.0); + // Modifying copy doesn't affect original + b(0, 0) = 99.0; + REQUIRE(a(0, 0) == 5.0); + + Tensor c(std::move(b)); + REQUIRE(c(0, 0) == 99.0); + REQUIRE(c.size() == 6); +} + +// ============================================================================ +// Tensor indexing +// ============================================================================ + +TEST_CASE("Tensor 1D indexing") +{ + Tensor t({4}, 0); + t[0] = 10; + t[1] = 20; + t[2] = 30; + t[3] = 40; + REQUIRE(t(0) == 10); + REQUIRE(t(1) == 20); + REQUIRE(t(2) == 30); + REQUIRE(t(3) == 40); +} + +TEST_CASE("Tensor 2D indexing (row-major)") +{ + // Layout: [[1, 2, 3], [4, 5, 6]] + Tensor t({2, 3}, 0); + int val = 1; + for (size_t i = 0; i < 2; ++i) + for (size_t j = 0; j < 3; ++j) + t(i, j) = val++; + + REQUIRE(t(0, 0) == 1); + REQUIRE(t(0, 2) == 3); + REQUIRE(t(1, 0) == 4); + REQUIRE(t(1, 2) == 6); + // Flat index should match row-major order + REQUIRE(t[0] == 1); + REQUIRE(t[3] == 4); + REQUIRE(t[5] == 6); +} + +TEST_CASE("Tensor 3D indexing") +{ + // 2x3x4 tensor + Tensor t({2, 3, 4}, 0); + t(1, 2, 3) = 42; + // Flat index: 1*12 + 2*4 + 3 = 23 + REQUIRE(t[23] == 42); + REQUIRE(t(1, 2, 3) == 42); +} + +// ============================================================================ +// Tensor assignment +// ============================================================================ + +TEST_CASE("Tensor initializer_list assignment") +{ + Tensor t; + t = {1.0, 2.0, 3.0}; + REQUIRE(t.size() == 3); + REQUIRE(t.shape(0) == 3); + REQUIRE(t[0] == 1.0); + REQUIRE(t[2] == 3.0); +} + +// ============================================================================ +// Tensor mutation +// ============================================================================ + +TEST_CASE("Tensor resize") +{ + Tensor t({2, 3}, 1.0); + REQUIRE(t.size() == 6); + t.resize({4, 5}); + REQUIRE(t.size() == 20); + REQUIRE(t.shape(0) == 4); + REQUIRE(t.shape(1) == 5); +} + +TEST_CASE("Tensor reshape") +{ + Tensor t({12}, 0); + for (size_t i = 0; i < 12; ++i) + t[i] = static_cast(i); + + t.reshape({3, 4}); + REQUIRE(t.shape(0) == 3); + REQUIRE(t.shape(1) == 4); + REQUIRE(t.size() == 12); + // Data unchanged, just reinterpreted + REQUIRE(t(0, 0) == 0); + REQUIRE(t(1, 0) == 4); // row 1, col 0 = flat index 4 + REQUIRE(t(2, 3) == 11); // row 2, col 3 = flat index 11 +} + +TEST_CASE("Tensor fill") +{ + Tensor t({3, 3}, 0.0); + t.fill(42.0); + for (size_t i = 0; i < t.size(); ++i) + REQUIRE(t[i] == 42.0); +} + +// ============================================================================ +// Tensor iterators +// ============================================================================ + +TEST_CASE("Tensor iterators") +{ + Tensor t({4}, 0); + t = {10, 20, 30, 40}; + int sum = 0; + for (auto val : t) + sum += val; + REQUIRE(sum == 100); +} + +// ============================================================================ +// Tensor reductions +// ============================================================================ + +TEST_CASE("Tensor sum (full)") +{ + Tensor t({3}, 0.0); + t = {1.0, 2.0, 3.0}; + REQUIRE(t.sum() == 6.0); +} + +TEST_CASE("Tensor sum (axis) on 2D") +{ + // [[1, 2, 3], + // [4, 5, 6]] + Tensor t({2, 3}, 0); + int v = 1; + for (size_t i = 0; i < 2; ++i) + for (size_t j = 0; j < 3; ++j) + t(i, j) = v++; + + // Sum along axis 0 -> [5, 7, 9] + Tensor s0 = t.sum(0); + REQUIRE(s0.size() == 3); + REQUIRE(s0[0] == 5); + REQUIRE(s0[1] == 7); + REQUIRE(s0[2] == 9); + + // Sum along axis 1 -> [6, 15] + Tensor s1 = t.sum(1); + REQUIRE(s1.size() == 2); + REQUIRE(s1[0] == 6); + REQUIRE(s1[1] == 15); +} + +TEST_CASE("Tensor sum (axis) on 3D") +{ + // 2x3x2 tensor filled with sequential values 1..12 + Tensor t({2, 3, 2}, 0); + int v = 1; + for (size_t i = 0; i < 2; ++i) + for (size_t j = 0; j < 3; ++j) + for (size_t k = 0; k < 2; ++k) + t(i, j, k) = v++; + + // Sum along axis 1 (middle) -> 2x2, each sums 3 values + // [0,0]: t(0,0,0)+t(0,1,0)+t(0,2,0) = 1+3+5 = 9 + // [0,1]: t(0,0,1)+t(0,1,1)+t(0,2,1) = 2+4+6 = 12 + // [1,0]: t(1,0,0)+t(1,1,0)+t(1,2,0) = 7+9+11 = 27 + // [1,1]: t(1,0,1)+t(1,1,1)+t(1,2,1) = 8+10+12 = 30 + Tensor s = t.sum(1); + REQUIRE(s.shape(0) == 2); + REQUIRE(s.shape(1) == 2); + REQUIRE(s(0, 0) == 9); + REQUIRE(s(0, 1) == 12); + REQUIRE(s(1, 0) == 27); + REQUIRE(s(1, 1) == 30); +} + +TEST_CASE("Tensor prod") +{ + Tensor t({4}, 0); + t = {1, 2, 3, 4}; + REQUIRE(t.prod() == 24); +} + +TEST_CASE("Tensor any and all") +{ + Tensor t({4}, false); + REQUIRE(!t.any()); + REQUIRE(!t.all()); + + // Set one element true + t.data()[0] = true; + REQUIRE(t.any()); + REQUIRE(!t.all()); + + // Set all true + for (size_t i = 0; i < t.size(); ++i) + t.data()[i] = true; + REQUIRE(t.any()); + REQUIRE(t.all()); +} + +TEST_CASE("Tensor argmin") +{ + Tensor t({5}, 0.0); + t = {3.0, 1.0, 4.0, 0.5, 2.0}; + REQUIRE(t.argmin() == 3); +} + +TEST_CASE("Tensor flip") +{ + Tensor t({5}, 0); + t = {1, 2, 3, 4, 5}; + Tensor f = t.flip(0); + REQUIRE(f[0] == 5); + REQUIRE(f[1] == 4); + REQUIRE(f[2] == 3); + REQUIRE(f[3] == 2); + REQUIRE(f[4] == 1); +} + +TEST_CASE("Tensor flip 2D") +{ + // [[1, 2], [3, 4], [5, 6]] + Tensor t({3, 2}, 0); + t(0, 0) = 1; + t(0, 1) = 2; + t(1, 0) = 3; + t(1, 1) = 4; + t(2, 0) = 5; + t(2, 1) = 6; + + // Flip axis 0 reverses rows -> [[5,6],[3,4],[1,2]] + Tensor f = t.flip(0); + REQUIRE(f(0, 0) == 5); + REQUIRE(f(0, 1) == 6); + REQUIRE(f(1, 0) == 3); + REQUIRE(f(2, 0) == 1); +} + +// ============================================================================ +// Tensor operators +// ============================================================================ + +TEST_CASE("Tensor scalar compound assignment") +{ + Tensor t({3}, 0.0); + t = {2.0, 4.0, 6.0}; + + t += 1.0; + REQUIRE(t[0] == 3.0); + REQUIRE(t[1] == 5.0); + + t -= 1.0; + REQUIRE(t[0] == 2.0); + + t *= 3.0; + REQUIRE(t[0] == 6.0); + REQUIRE(t[1] == 12.0); + + t /= 2.0; + REQUIRE(t[0] == 3.0); + REQUIRE(t[1] == 6.0); +} + +TEST_CASE("Tensor element-wise arithmetic") +{ + Tensor a({3}, 0.0); + Tensor b({3}, 0.0); + a = {1.0, 2.0, 3.0}; + b = {4.0, 5.0, 6.0}; + + Tensor c = a + b; + REQUIRE(c[0] == 5.0); + REQUIRE(c[1] == 7.0); + REQUIRE(c[2] == 9.0); + + c = a - b; + REQUIRE(c[0] == -3.0); + + c = a / b; + REQUIRE(c[0] == 0.25); +} + +TEST_CASE("Tensor scalar arithmetic") +{ + Tensor a({3}, 0.0); + a = {1.0, 2.0, 3.0}; + + Tensor b = a + 10.0; + REQUIRE(b[0] == 11.0); + REQUIRE(b[2] == 13.0); + + b = a - 1.0; + REQUIRE(b[0] == 0.0); + + b = a * 2.0; + REQUIRE(b[0] == 2.0); + REQUIRE(b[2] == 6.0); + + // Non-member scalar * tensor (commutativity) + b = 2.0 * a; + REQUIRE(b[0] == 2.0); + REQUIRE(b[2] == 6.0); + + // Non-member scalar + tensor + b = 10.0 + a; + REQUIRE(b[0] == 11.0); +} + +TEST_CASE("Tensor compound addition with tensor") +{ + Tensor a({3}, 0.0); + Tensor b({3}, 0.0); + a = {1.0, 2.0, 3.0}; + b = {10.0, 20.0, 30.0}; + a += b; + REQUIRE(a[0] == 11.0); + REQUIRE(a[1] == 22.0); + REQUIRE(a[2] == 33.0); +} + +TEST_CASE("Tensor comparison operators") +{ + Tensor t({4}, 0.0); + t = {1.0, 2.0, 3.0, 4.0}; + + Tensor r = t < 3.0; + REQUIRE(r.data()[0] == true); + REQUIRE(r.data()[1] == true); + REQUIRE(r.data()[2] == false); + REQUIRE(r.data()[3] == false); + + r = t >= 3.0; + REQUIRE(r.data()[0] == false); + REQUIRE(r.data()[2] == true); + REQUIRE(r.data()[3] == true); + + r = t <= 2.0; + REQUIRE(r.data()[0] == true); + REQUIRE(r.data()[1] == true); + REQUIRE(r.data()[2] == false); + + r = t > 3.0; + REQUIRE(r.data()[0] == false); + REQUIRE(r.data()[3] == true); +} + +TEST_CASE("Tensor element-wise comparison") +{ + Tensor a({3}, 0.0); + Tensor b({3}, 0.0); + a = {1.0, 5.0, 3.0}; + b = {2.0, 4.0, 3.0}; + + Tensor r = a < b; + REQUIRE(r.data()[0] == true); + REQUIRE(r.data()[1] == false); + REQUIRE(r.data()[2] == false); +} + +TEST_CASE("Tensor mixed-type multiply") +{ + Tensor a({3}, 0); + Tensor b({3}, 0.0); + a = {2, 3, 4}; + b = {1.5, 2.5, 3.5}; + + Tensor c = a * b; + REQUIRE(c[0] == 3.0); + REQUIRE(c[1] == 7.5); + REQUIRE(c[2] == 14.0); +} + +TEST_CASE("Tensor mixed-type divide") +{ + Tensor a({3}, 0.0); + Tensor b({3}, 0); + a = {10.0, 20.0, 30.0}; + b = {2, 4, 5}; + + Tensor c = a / b; + REQUIRE(c[0] == 5.0); + REQUIRE(c[1] == 5.0); + REQUIRE(c[2] == 6.0); +} + +// ============================================================================ +// Tensor bool specialization +// ============================================================================ + +TEST_CASE("Tensor storage") +{ + // Tensor uses unsigned char internally to avoid std::vector proxy + Tensor t({4}, false); + t.data()[0] = true; + t.data()[2] = true; + REQUIRE(t.any()); + REQUIRE(!t.all()); + REQUIRE(t.data()[0] == true); + REQUIRE(t.data()[1] == false); +} + +// ============================================================================ +// View (via Tensor accessors) +// ============================================================================ + +TEST_CASE("Tensor slice axis 0 (2D)") +{ + // [[1, 2, 3], [4, 5, 6]] + Tensor t({2, 3}, 0); + int v = 1; + for (size_t i = 0; i < 2; ++i) + for (size_t j = 0; j < 3; ++j) + t(i, j) = v++; + + auto r0 = t.slice(0); + REQUIRE(r0.size() == 3); + REQUIRE(r0[0] == 1); + REQUIRE(r0[1] == 2); + REQUIRE(r0[2] == 3); + + auto r1 = t.slice(1); + REQUIRE(r1[0] == 4); + REQUIRE(r1[1] == 5); + REQUIRE(r1[2] == 6); + + // Writing through view modifies the tensor + r0[1] = 99; + REQUIRE(t(0, 1) == 99); +} + +TEST_CASE("Tensor slice axis 1 (2D)") +{ + // [[1, 2], [3, 4], [5, 6]] + Tensor t({3, 2}, 0); + t(0, 0) = 1; + t(0, 1) = 2; + t(1, 0) = 3; + t(1, 1) = 4; + t(2, 0) = 5; + t(2, 1) = 6; + + auto c0 = t.slice(all, 0); + REQUIRE(c0.size() == 3); + REQUIRE(c0[0] == 1); + REQUIRE(c0[1] == 3); + REQUIRE(c0[2] == 5); + + auto c1 = t.slice(all, 1); + REQUIRE(c1[0] == 2); + REQUIRE(c1[1] == 4); + REQUIRE(c1[2] == 6); + + // Write through column view + c1[0] = 77; + REQUIRE(t(0, 1) == 77); +} + +TEST_CASE("Tensor slice with range") +{ + Tensor t({6}, 0); + t = {10, 20, 30, 40, 50, 60}; + + // range(start, end) + auto s = t.slice(range(1, 4)); + REQUIRE(s.size() == 3); + REQUIRE(s[0] == 20); + REQUIRE(s[1] == 30); + REQUIRE(s[2] == 40); + + // range(end) from start — range(3) means [0, 3) + auto s2 = t.slice(range(3)); + REQUIRE(s2.size() == 3); + REQUIRE(s2[0] == 10); + REQUIRE(s2[2] == 30); + + // range(start, SIZE_MAX) to end + auto s3 = t.slice(range(3, 6)); + REQUIRE(s3.size() == 3); + REQUIRE(s3[0] == 40); + REQUIRE(s3[2] == 60); + + // Write through slice + s[0] = 99; + REQUIRE(t[1] == 99); +} + +TEST_CASE("Tensor flat view") +{ + Tensor t({2, 3}, 0); + int v = 1; + for (size_t i = 0; i < 2; ++i) + for (size_t j = 0; j < 3; ++j) + t(i, j) = v++; + + auto f = t.flat(); + REQUIRE(f.size() == 6); + REQUIRE(f[0] == 1); + REQUIRE(f[5] == 6); +} + +TEST_CASE("Tensor slice on 3D") +{ + // 2x3x4 tensor + Tensor t({2, 3, 4}, 0); + int v = 0; + for (size_t i = 0; i < 2; ++i) + for (size_t j = 0; j < 3; ++j) + for (size_t k = 0; k < 4; ++k) + t(i, j, k) = v++; + + // slice(1) -> fix axis 0 at 1 -> 3x4 view + auto s = t.slice(1); + REQUIRE(s.size() == 12); + // t(1,0,0) = 12, t(1,0,1) = 13, ... + REQUIRE(s(0, 0) == 12); + REQUIRE(s(0, 1) == 13); + REQUIRE(s(2, 3) == 23); + + // slice(all, 2) -> fix axis 1 at 2 -> 2x4 view + auto s2 = t.slice(all, 2); + REQUIRE(s2.size() == 8); + // t(0,2,0)=8, t(0,2,1)=9, t(1,2,0)=20 + REQUIRE(s2(0, 0) == 8); + REQUIRE(s2(0, 1) == 9); + REQUIRE(s2(1, 0) == 20); +} + +TEST_CASE("Tensor multi-axis slice") +{ + // 2x3x4 tensor with sequential values + Tensor t({2, 3, 4}, 0); + int v = 0; + for (size_t i = 0; i < 2; ++i) + for (size_t j = 0; j < 3; ++j) + for (size_t k = 0; k < 4; ++k) + t(i, j, k) = v++; + + // slice(1, 2) -> fix axes 0 and 1 -> 1D view of 4 elements + // Equivalent to numpy t[1, 2, :] -> t(1,2,0..3) = [20, 21, 22, 23] + auto s = t.slice(1, 2); + REQUIRE(s.size() == 4); + REQUIRE(s[0] == 20); + REQUIRE(s[1] == 21); + REQUIRE(s[3] == 23); + + // slice(all, 1, range(1, 3)) -> keep axis 0, fix axis 1, range on axis 2 + // Equivalent to numpy t[:, 1, 1:3] + // t(0,1,1)=5, t(0,1,2)=6, t(1,1,1)=17, t(1,1,2)=18 + auto s2 = t.slice(all, 1, range(1, 3)); + REQUIRE(s2.size() == 4); + REQUIRE(s2.ndim() == 2); + REQUIRE(s2(0, 0) == 5); + REQUIRE(s2(0, 1) == 6); + REQUIRE(s2(1, 0) == 17); + REQUIRE(s2(1, 1) == 18); + + // slice(0, range(0, 2)) -> fix axis 0 at 0, range on axis 1 + // Equivalent to numpy t[0, 0:2, :] -> shape (2, 4) + auto s3 = t.slice(0, range(0, 2)); + REQUIRE(s3.ndim() == 2); + REQUIRE(s3.shape(0) == 2); + REQUIRE(s3.shape(1) == 4); + REQUIRE(s3(0, 0) == 0); // t(0,0,0) + REQUIRE(s3(1, 3) == 7); // t(0,1,3) +} + +// ============================================================================ +// View assignment and arithmetic +// ============================================================================ + +TEST_CASE("View scalar assignment (fill)") +{ + Tensor t({2, 3}, 0.0); + auto r = t.slice(0); + r = 7.0; + REQUIRE(t(0, 0) == 7.0); + REQUIRE(t(0, 1) == 7.0); + REQUIRE(t(0, 2) == 7.0); + REQUIRE(t(1, 0) == 0.0); // Other row unchanged +} + +TEST_CASE("View initializer_list assignment") +{ + Tensor t({2, 3}, 0.0); + auto r = t.slice(1); + r = {10.0, 20.0, 30.0}; + REQUIRE(t(1, 0) == 10.0); + REQUIRE(t(1, 1) == 20.0); + REQUIRE(t(1, 2) == 30.0); +} + +TEST_CASE("View copy assignment (deep copy)") +{ + Tensor t({2, 3}, 0.0); + t.slice(0) = {1.0, 2.0, 3.0}; + t.slice(1) = {4.0, 5.0, 6.0}; + + // Copy row 0 into row 1 + t.slice(1) = t.slice(0); + REQUIRE(t(1, 0) == 1.0); + REQUIRE(t(1, 1) == 2.0); + REQUIRE(t(1, 2) == 3.0); +} + +TEST_CASE("View compound operators") +{ + Tensor t({2, 3}, 0.0); + t.slice(0) = {1.0, 2.0, 3.0}; + + t.slice(0) *= 2.0; + REQUIRE(t(0, 0) == 2.0); + REQUIRE(t(0, 1) == 4.0); + + t.slice(0) /= 2.0; + REQUIRE(t(0, 0) == 1.0); + REQUIRE(t(0, 1) == 2.0); +} + +TEST_CASE("View assignment from tensor") +{ + Tensor t({2, 3}, 0.0); + Tensor vals({3}, 0.0); + vals = {7.0, 8.0, 9.0}; + + t.slice(1) = vals; + REQUIRE(t(1, 0) == 7.0); + REQUIRE(t(1, 1) == 8.0); + REQUIRE(t(1, 2) == 9.0); +} + +TEST_CASE("View compound addition from tensor") +{ + Tensor t({2, 3}, 0.0); + t.slice(0) = {1.0, 2.0, 3.0}; + Tensor vals({3}, 0.0); + vals = {10.0, 20.0, 30.0}; + + t.slice(0) += vals; + REQUIRE(t(0, 0) == 11.0); + REQUIRE(t(0, 1) == 22.0); + REQUIRE(t(0, 2) == 33.0); +} + +TEST_CASE("View sum") +{ + Tensor t({2, 3}, 0.0); + t.slice(0) = {1.0, 2.0, 3.0}; + t.slice(1) = {4.0, 5.0, 6.0}; + + REQUIRE(t.slice(0).sum() == 6.0); + REQUIRE(t.slice(1).sum() == 15.0); +} + +TEST_CASE("View iteration") +{ + Tensor t({2, 3}, 0); + t.slice(0) = {1, 2, 3}; + + int sum = 0; + for (auto val : t.slice(0)) + sum += val; + REQUIRE(sum == 6); +} + +TEST_CASE("View sub-slice") +{ + Tensor t({6}, 0); + t = {10, 20, 30, 40, 50, 60}; + + auto s = t.slice(range(1, 5)); // [20, 30, 40, 50] + auto ss = s.slice(range(1, 3)); // [30, 40] + REQUIRE(ss.size() == 2); + REQUIRE(ss[0] == 30); + REQUIRE(ss[1] == 40); +} + +TEST_CASE("Tensor from View") +{ + Tensor t({2, 3}, 0.0); + t.slice(0) = {1.0, 2.0, 3.0}; + + // Construct a new tensor from a view (copies data) + Tensor t2(t.slice(0)); + REQUIRE(t2.size() == 3); + REQUIRE(t2[0] == 1.0); + REQUIRE(t2[2] == 3.0); + + // Modifying the new tensor doesn't affect the original + t2[0] = 99.0; + REQUIRE(t(0, 0) == 1.0); +} + +// ============================================================================ +// Const View +// ============================================================================ + +TEST_CASE("Const tensor produces const views") +{ + Tensor t({2, 3}, 0.0); + int v = 1; + for (size_t i = 0; i < 2; ++i) + for (size_t j = 0; j < 3; ++j) + t(i, j) = v++; + + const Tensor& ct = t; + auto r = ct.slice(0); // View + REQUIRE(r[0] == 1.0); + REQUIRE(r[2] == 3.0); + + auto c = ct.slice(all, 1); + REQUIRE(c[0] == 2.0); + REQUIRE(c[1] == 5.0); +} + +// ============================================================================ +// StaticTensor2D +// ============================================================================ + +TEST_CASE("StaticTensor2D basics") +{ + StaticTensor2D t; + REQUIRE(t.size() == 12); + REQUIRE(t.shape()[0] == 3); + REQUIRE(t.shape()[1] == 4); + + // Default-initialized to zero + REQUIRE(t(0, 0) == 0.0); + + t(1, 2) = 42.0; + REQUIRE(t(1, 2) == 42.0); + // Flat data: row 1, col 2 = index 1*4 + 2 = 6 + REQUIRE(t.data()[6] == 42.0); +} + +TEST_CASE("StaticTensor2D fill") +{ + StaticTensor2D t; + t.fill(5); + for (size_t i = 0; i < t.size(); ++i) + REQUIRE(t.data()[i] == 5); +} + +TEST_CASE("StaticTensor2D iteration") +{ + StaticTensor2D t; + t.fill(1); + int sum = 0; + for (auto val : t) + sum += val; + REQUIRE(sum == 6); +} + +TEST_CASE("StaticTensor2D slice") +{ + StaticTensor2D t; + t(0, 0) = 1; + t(0, 1) = 2; + t(1, 0) = 3; + t(1, 1) = 4; + t(2, 0) = 5; + t(2, 1) = 6; + + // slice(1) = row 1 (fix axis 0 at 1) + auto r1 = t.slice(1); + REQUIRE(r1.size() == 2); + REQUIRE(r1[0] == 3); + REQUIRE(r1[1] == 4); + + // slice(all, 0) = column 0 (fix axis 1 at 0) + auto c0 = t.slice(all, 0); + REQUIRE(c0.size() == 3); + REQUIRE(c0[0] == 1); + REQUIRE(c0[1] == 3); + REQUIRE(c0[2] == 5); +} + +TEST_CASE("StaticTensor2D flat view") +{ + StaticTensor2D t; + t(0, 0) = 1.0; + t(0, 1) = 2.0; + t(1, 0) = 3.0; + t(1, 1) = 4.0; + + auto f = t.flat(); + REQUIRE(f.size() == 4); + f = 0.0; + REQUIRE(t(0, 0) == 0.0); + REQUIRE(t(1, 1) == 0.0); +} + +// ============================================================================ +// Non-member functions +// ============================================================================ + +TEST_CASE("zeros") +{ + auto t = zeros({3, 4}); + REQUIRE(t.size() == 12); + for (size_t i = 0; i < t.size(); ++i) + REQUIRE(t[i] == 0.0); +} + +TEST_CASE("zeros_like") +{ + Tensor a({2, 5}, 7.0); + auto b = zeros_like(a); + REQUIRE(b.size() == 10); + REQUIRE(b.shape(0) == 2); + REQUIRE(b.shape(1) == 5); + for (size_t i = 0; i < b.size(); ++i) + REQUIRE(b[i] == 0.0); +} + +TEST_CASE("full_like") +{ + Tensor a({4}, 0); + auto b = full_like(a, 42); + REQUIRE(b.size() == 4); + for (size_t i = 0; i < b.size(); ++i) + REQUIRE(b[i] == 42); +} + +TEST_CASE("linspace") +{ + auto t = linspace(0.0, 1.0, 5); + REQUIRE(t.size() == 5); + REQUIRE(t[0] == 0.0); + REQUIRE(t[4] == 1.0); + REQUIRE_THAT(t[1], Catch::Matchers::WithinRel(0.25, 1e-12)); + REQUIRE_THAT(t[2], Catch::Matchers::WithinRel(0.5, 1e-12)); + REQUIRE_THAT(t[3], Catch::Matchers::WithinRel(0.75, 1e-12)); +} + +TEST_CASE("concatenate") +{ + Tensor a({3}, 0); + Tensor b({2}, 0); + a = {1, 2, 3}; + b = {4, 5}; + + auto c = concatenate(a, b); + REQUIRE(c.size() == 5); + REQUIRE(c[0] == 1); + REQUIRE(c[2] == 3); + REQUIRE(c[3] == 4); + REQUIRE(c[4] == 5); +} + +TEST_CASE("log") +{ + Tensor t({3}, 0.0); + t = {1.0, std::exp(1.0), std::exp(2.0)}; + + auto r = log(t); + REQUIRE_THAT(r[0], Catch::Matchers::WithinAbs(0.0, 1e-12)); + REQUIRE_THAT(r[1], Catch::Matchers::WithinAbs(1.0, 1e-12)); + REQUIRE_THAT(r[2], Catch::Matchers::WithinAbs(2.0, 1e-12)); +} + +TEST_CASE("abs") +{ + Tensor t({4}, 0.0); + t = {-3.0, -1.0, 0.0, 2.0}; + + auto r = abs(t); + REQUIRE(r[0] == 3.0); + REQUIRE(r[1] == 1.0); + REQUIRE(r[2] == 0.0); + REQUIRE(r[3] == 2.0); +} + +TEST_CASE("where") +{ + Tensor cond({4}, false); + cond.data()[0] = true; + cond.data()[2] = true; + + Tensor vals({4}, 0.0); + vals = {10.0, 20.0, 30.0, 40.0}; + + auto r = where(cond, vals, -1.0); + REQUIRE(r[0] == 10.0); + REQUIRE(r[1] == -1.0); + REQUIRE(r[2] == 30.0); + REQUIRE(r[3] == -1.0); +} + +TEST_CASE("nan_to_num") +{ + Tensor t({4}, 0.0); + t[0] = 1.0; + t[1] = std::nan(""); + t[2] = std::numeric_limits::infinity(); + t[3] = -std::numeric_limits::infinity(); + + auto r = nan_to_num(t); + REQUIRE(r[0] == 1.0); + REQUIRE(r[1] == 0.0); // NaN -> 0 + REQUIRE(r[2] == std::numeric_limits::max()); // +inf -> max + REQUIRE(r[3] == std::numeric_limits::lowest()); // -inf -> lowest +} + +// ============================================================================ +// is_tensor trait +// ============================================================================ + +TEST_CASE("is_tensor trait") +{ + REQUIRE(is_tensor>::value); + REQUIRE(is_tensor>::value); + REQUIRE(is_tensor>::value); + REQUIRE(!is_tensor::value); + REQUIRE(!is_tensor>::value); +} diff --git a/tests/regression_tests/cpp_driver/driver.cpp b/tests/regression_tests/cpp_driver/driver.cpp index a99c97b64e..a6c3e65103 100644 --- a/tests/regression_tests/cpp_driver/driver.cpp +++ b/tests/regression_tests/cpp_driver/driver.cpp @@ -2,6 +2,8 @@ #include #endif +#include + #include "openmc/capi.h" #include "openmc/cell.h" #include "openmc/error.h" diff --git a/vendor/xtensor b/vendor/xtensor deleted file mode 160000 index 3634f2ded1..0000000000 --- a/vendor/xtensor +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3634f2ded19e0cf38208c8b86cea9e1d7c8e397d diff --git a/vendor/xtl b/vendor/xtl deleted file mode 160000 index a7c1c5444d..0000000000 --- a/vendor/xtl +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a7c1c5444dfc57f76620391af4c94785ff82c8d6 From d4dc08961897c7990665fa6dcae386bf0c0fe555 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Wed, 18 Feb 2026 17:09:39 +0200 Subject: [PATCH 02/38] Remove redundant check (#3812) --- src/tallies/filter_energy.cpp | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/tallies/filter_energy.cpp b/src/tallies/filter_energy.cpp index 0b954cce3a..566b5710ad 100644 --- a/src/tallies/filter_energy.cpp +++ b/src/tallies/filter_energy.cpp @@ -60,13 +60,8 @@ void EnergyFilter::get_all_bins( const Particle& p, TallyEstimator estimator, FilterMatch& match) const { if (p.g() != C_NONE && matches_transport_groups_) { - if (estimator == TallyEstimator::TRACKLENGTH) { - match.bins_.push_back(data::mg.num_energy_groups_ - p.g() - 1); - } else { - match.bins_.push_back(data::mg.num_energy_groups_ - p.g_last() - 1); - } + match.bins_.push_back(data::mg.num_energy_groups_ - p.g_last() - 1); match.weights_.push_back(1.0); - } else { // Get the pre-collision energy of the particle. auto E = p.E_last(); From 6d6b051507c7e70958beb1a23bd69bf6172617f4 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Wed, 18 Feb 2026 18:18:05 +0100 Subject: [PATCH 03/38] avoid need to set particles and batches for dagmc models when calling convert_to_multigroup (#3801) Co-authored-by: Jon Shimwell Co-authored-by: Patrick Shriwise Co-authored-by: GuySten <62616591+GuySten@users.noreply.github.com> --- openmc/model/model.py | 7 +++ .../dagmc/test_convert_to_multigroup.py | 53 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 tests/unit_tests/dagmc/test_convert_to_multigroup.py diff --git a/openmc/model/model.py b/openmc/model/model.py index a512c4d380..0920b79e48 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -2584,9 +2584,16 @@ class Model: # TODO: Can this be done without having to init/finalize? for univ in self.geometry.get_all_universes().values(): if isinstance(univ, openmc.DAGMCUniverse): + # Initialize in stochastic volume mode (non-transport mode) + # This mode doesn't require + # valid transport settings like particles/batches + original_run_mode = self.settings.run_mode + self.settings.run_mode = 'volume' self.init_lib(directory=tmpdir) self.sync_dagmc_universes() self.finalize_lib() + # Restore original run mode + self.settings.run_mode = original_run_mode break # Make sure all materials have a name, and that the name is a valid HDF5 diff --git a/tests/unit_tests/dagmc/test_convert_to_multigroup.py b/tests/unit_tests/dagmc/test_convert_to_multigroup.py new file mode 100644 index 0000000000..069dc658e5 --- /dev/null +++ b/tests/unit_tests/dagmc/test_convert_to_multigroup.py @@ -0,0 +1,53 @@ +"""Test that convert_to_multigroup works with DAGMC models without requiring +particles/batches to be set beforehand. +""" + +from pathlib import Path +import pytest +import openmc +import openmc.lib + +pytestmark = pytest.mark.skipif( + not openmc.lib._dagmc_enabled(), + reason="DAGMC CAD geometry is not enabled.") + + +def test_convert_to_multigroup_without_particles_batches(run_in_tmpdir): + """Test that convert_to_multigroup works with DAGMC model without + setting particles/batches beforehand.""" + openmc.reset_auto_ids() + + mat = openmc.Material(name="mat") + mat.add_nuclide("Fe56", 1.0) + mat.set_density("g/cm3", 7.0) + + # Use minimal tetrahedral DAGMC file + dagmc_file = Path(__file__).parent / "dagmc_tetrahedral_no_graveyard.h5m" + dagmc_univ = openmc.DAGMCUniverse(dagmc_file, auto_geom_ids=True) + bound_dagmc_univ = dagmc_univ.bounded_universe(padding_distance=1) + + # Create model WITHOUT setting particles or batches + model = openmc.Model() + model.materials = openmc.Materials([mat]) + model.geometry = openmc.Geometry(bound_dagmc_univ) + model.settings = openmc.Settings() # Note: no particles or batches set! + + model.settings.run_mode = 'fixed source' + + # Create a point source + my_source = openmc.IndependentSource() + my_source.space = openmc.stats.Point((0.25, 0.25, 0.25)) + my_source.energy = openmc.stats.delta_function(14e6) + model.settings.source = my_source + + # This should work without requiring particles/batches to be set + # convert_to_multigroup handles initialization internally using non-transport mode + model.convert_to_multigroup( + method='material_wise', + groups='CASMO-2', + nparticles=10, + overwrite_mgxs_library=True + ) + + # Verify the model was converted successfully + assert model.settings.energy_mode == 'multi-group' From f007c85a5033a465b44064a856045dd8f041318c Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Thu, 19 Feb 2026 10:36:12 +0200 Subject: [PATCH 04/38] Check for positive radii (#3813) --- openmc/surface.py | 31 +++++++++++--------- tests/unit_tests/test_pin.py | 5 ---- tests/unit_tests/test_surface.py | 49 ++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 18 deletions(-) diff --git a/openmc/surface.py b/openmc/surface.py index 97aca43b32..c2afeb613a 100644 --- a/openmc/surface.py +++ b/openmc/surface.py @@ -38,10 +38,13 @@ class SurfaceCoefficient: value : float or str Value of the coefficient (float) or the name of the coefficient that it is equivalent to (str). + positive : bool + Does the surface coefficient must be positive. Defaults to False. """ - def __init__(self, value): + def __init__(self, value, positive=False): self.value = value + self.positive = positive def __get__(self, instance, owner=None): if instance is None: @@ -56,6 +59,8 @@ class SurfaceCoefficient: if isinstance(self.value, Real): raise AttributeError('This coefficient is read-only') check_type(f'{self.value} coefficient', value, Real) + if self.positive: + check_greater_than(f'{self.value} coefficient', value, 0.0) instance._coefficients[self.value] = value @@ -1261,7 +1266,7 @@ class Cylinder(QuadricMixin, Surface): x0 = SurfaceCoefficient('x0') y0 = SurfaceCoefficient('y0') z0 = SurfaceCoefficient('z0') - r = SurfaceCoefficient('r') + r = SurfaceCoefficient('r', positive=True) dx = SurfaceCoefficient('dx') dy = SurfaceCoefficient('dy') dz = SurfaceCoefficient('dz') @@ -1427,7 +1432,7 @@ class XCylinder(QuadricMixin, Surface): x0 = SurfaceCoefficient(0.) y0 = SurfaceCoefficient('y0') z0 = SurfaceCoefficient('z0') - r = SurfaceCoefficient('r') + r = SurfaceCoefficient('r', positive=True) dx = SurfaceCoefficient(1.) dy = SurfaceCoefficient(0.) dz = SurfaceCoefficient(0.) @@ -1525,7 +1530,7 @@ class YCylinder(QuadricMixin, Surface): x0 = SurfaceCoefficient('x0') y0 = SurfaceCoefficient(0.) z0 = SurfaceCoefficient('z0') - r = SurfaceCoefficient('r') + r = SurfaceCoefficient('r', positive=True) dx = SurfaceCoefficient(0.) dy = SurfaceCoefficient(1.) dz = SurfaceCoefficient(0.) @@ -1623,7 +1628,7 @@ class ZCylinder(QuadricMixin, Surface): x0 = SurfaceCoefficient('x0') y0 = SurfaceCoefficient('y0') z0 = SurfaceCoefficient(0.) - r = SurfaceCoefficient('r') + r = SurfaceCoefficient('r', positive=True) dx = SurfaceCoefficient(0.) dy = SurfaceCoefficient(0.) dz = SurfaceCoefficient(1.) @@ -1723,7 +1728,7 @@ class Sphere(QuadricMixin, Surface): x0 = SurfaceCoefficient('x0') y0 = SurfaceCoefficient('y0') z0 = SurfaceCoefficient('z0') - r = SurfaceCoefficient('r') + r = SurfaceCoefficient('r', positive=True) def _get_base_coeffs(self): x0, y0, z0, r = self.x0, self.y0, self.z0, self.r @@ -1849,7 +1854,7 @@ class Cone(QuadricMixin, Surface): x0 = SurfaceCoefficient('x0') y0 = SurfaceCoefficient('y0') z0 = SurfaceCoefficient('z0') - r2 = SurfaceCoefficient('r2') + r2 = SurfaceCoefficient('r2', positive=True) dx = SurfaceCoefficient('dx') dy = SurfaceCoefficient('dy') dz = SurfaceCoefficient('dz') @@ -1985,7 +1990,7 @@ class XCone(QuadricMixin, Surface): x0 = SurfaceCoefficient('x0') y0 = SurfaceCoefficient('y0') z0 = SurfaceCoefficient('z0') - r2 = SurfaceCoefficient('r2') + r2 = SurfaceCoefficient('r2', positive=True) dx = SurfaceCoefficient(1.) dy = SurfaceCoefficient(0.) dz = SurfaceCoefficient(0.) @@ -2087,7 +2092,7 @@ class YCone(QuadricMixin, Surface): x0 = SurfaceCoefficient('x0') y0 = SurfaceCoefficient('y0') z0 = SurfaceCoefficient('z0') - r2 = SurfaceCoefficient('r2') + r2 = SurfaceCoefficient('r2', positive=True) dx = SurfaceCoefficient(0.) dy = SurfaceCoefficient(1.) dz = SurfaceCoefficient(0.) @@ -2189,7 +2194,7 @@ class ZCone(QuadricMixin, Surface): x0 = SurfaceCoefficient('x0') y0 = SurfaceCoefficient('y0') z0 = SurfaceCoefficient('z0') - r2 = SurfaceCoefficient('r2') + r2 = SurfaceCoefficient('r2', positive=True) dx = SurfaceCoefficient(0.) dy = SurfaceCoefficient(0.) dz = SurfaceCoefficient(1.) @@ -2292,9 +2297,9 @@ class TorusMixin: x0 = SurfaceCoefficient('x0') y0 = SurfaceCoefficient('y0') z0 = SurfaceCoefficient('z0') - a = SurfaceCoefficient('a') - b = SurfaceCoefficient('b') - c = SurfaceCoefficient('c') + a = SurfaceCoefficient('a', positive=True) + b = SurfaceCoefficient('b', positive=True) + c = SurfaceCoefficient('c', positive=True) def translate(self, vector, inplace=False): surf = self if inplace else self.clone() diff --git a/tests/unit_tests/test_pin.py b/tests/unit_tests/test_pin.py index 5496a3b73a..53665b1a60 100644 --- a/tests/unit_tests/test_pin.py +++ b/tests/unit_tests/test_pin.py @@ -46,11 +46,6 @@ def test_failure(pin_mats, good_radii): with pytest.raises(ValueError, match="length"): pin(good_surfaces[:len(pin_mats) - 2], pin_mats) - # Non-positive radii - rad = [openmc.ZCylinder(r=-0.1)] + good_surfaces[1:] - with pytest.raises(ValueError, match="index 0"): - pin(rad, pin_mats) - # Non-increasing radii surfs = tuple(reversed(good_surfaces)) with pytest.raises(ValueError, match="index 1"): diff --git a/tests/unit_tests/test_surface.py b/tests/unit_tests/test_surface.py index aaf591e05d..f5c0b8f8b6 100644 --- a/tests/unit_tests/test_surface.py +++ b/tests/unit_tests/test_surface.py @@ -195,6 +195,12 @@ def test_cylinder(): assert s.dy == -1 assert s.dz == 1 assert s.r == 2 + + # Check radius must be positive + with pytest.raises(ValueError): + openmc.Cylinder(x0=x0, y0=y0, z0=z0, dx=dx, dy=dy, dz=dz, r=0.0) + with pytest.raises(ValueError): + openmc.Cylinder(x0=x0, y0=y0, z0=z0, dx=dx, dy=dy, dz=dz, r=-1.0) # Check bounding box assert_infinite_bb(s) @@ -244,6 +250,12 @@ def test_xcylinder(): assert s.y0 == y assert s.z0 == z assert s.r == r + + # Check radius must be positive + with pytest.raises(ValueError): + openmc.XCylinder(y0=y, z0=z, r=0.0) + with pytest.raises(ValueError): + openmc.XCylinder(y0=y, z0=z, r=-1.0) # Check bounding box ll, ur = (+s).bounding_box @@ -290,6 +302,12 @@ def test_ycylinder(): assert s.x0 == x assert s.z0 == z assert s.r == r + + # Check radius must be positive + with pytest.raises(ValueError): + openmc.YCylinder(x0=x, z0=z, r=0.0) + with pytest.raises(ValueError): + openmc.YCylinder(x0=x, z0=z, r=-1.0) # Check bounding box ll, ur = (+s).bounding_box @@ -327,6 +345,12 @@ def test_zcylinder(): assert s.x0 == x assert s.y0 == y assert s.r == r + + # Check radius must be positive + with pytest.raises(ValueError): + openmc.ZCylinder(x0=x, y0=y, r=0.0) + with pytest.raises(ValueError): + openmc.ZCylinder(x0=x, y0=y, r=-1.0) # Check bounding box ll, ur = (+s).bounding_box @@ -365,6 +389,12 @@ def test_sphere(): assert s.y0 == y assert s.z0 == z assert s.r == r + + # Check radius must be positive + with pytest.raises(ValueError): + openmc.Sphere(x0=x, y0=y, z0=z, r=0.0) + with pytest.raises(ValueError): + openmc.Sphere(x0=x, y0=y, z0=z, r=-1.0) # Check bounding box ll, ur = (+s).bounding_box @@ -404,6 +434,12 @@ def cone_common(apex, r2, cls): assert s.y0 == y assert s.z0 == z assert s.r2 == r2 + + # Check radius must be positive + with pytest.raises(ValueError): + cls(x0=x, y0=y, z0=z, r2=0.0) + with pytest.raises(ValueError): + cls(x0=x, y0=y, z0=z, r2=-1.0) # Check bounding box assert_infinite_bb(s) @@ -442,6 +478,12 @@ def test_cone(): assert s.dy == -1 assert s.dz == 1 assert s.r2 == 4 + + # Check radius must be positive + with pytest.raises(ValueError): + openmc.Cone(x0=x0, y0=y0, z0=z0, dx=dx, dy=dy, dz=dz, r2=0.0) + with pytest.raises(ValueError): + openmc.Cone(x0=x0, y0=y0, z0=z0, dx=dx, dy=dy, dz=dz, r2=-1.0) # Check bounding box assert_infinite_bb(s) @@ -622,6 +664,13 @@ def torus_common(center, R, r1, r2, cls): assert s.a == R assert s.b == r1 assert s.c == r2 + + # Check radius must be positive + params = [(0.0, r1, r2), (R, 0.0, r2), (R, r1, 0.0), + (-1.0, r1, r2), (R, -1.0, r2), (R, r1, -1.0)] + for a,b,c in params: + with pytest.raises(ValueError): + cls(x0=x, y0=y, z0=z, a=a, b=b, c=c) # evaluate method assert s.evaluate((x, y, z)) > 0.0 From 417343c9203c9337547d1eeeefb8a9fd0ec536fd Mon Sep 17 00:00:00 2001 From: Kevin Sawatzky <66632997+nuclearkevin@users.noreply.github.com> Date: Thu, 19 Feb 2026 06:25:19 -0600 Subject: [PATCH 05/38] Implement S2 directional sampling in the random ray solver (#3811) --- include/openmc/constants.h | 2 +- include/openmc/random_ray/random_ray.h | 1 + openmc/settings.py | 8 +- src/random_ray/random_ray.cpp | 26 ++++++ src/random_ray/random_ray_simulation.cpp | 15 +++- src/settings.cpp | 2 + .../random_ray_s2/__init__.py | 0 .../random_ray_s2/inputs_true.dat | 69 ++++++++++++++++ .../random_ray_s2/results_true.dat | 21 +++++ tests/regression_tests/random_ray_s2/test.py | 82 +++++++++++++++++++ 10 files changed, 220 insertions(+), 6 deletions(-) create mode 100644 tests/regression_tests/random_ray_s2/__init__.py create mode 100644 tests/regression_tests/random_ray_s2/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_s2/results_true.dat create mode 100644 tests/regression_tests/random_ray_s2/test.py diff --git a/include/openmc/constants.h b/include/openmc/constants.h index 38238598eb..e62b2941c7 100644 --- a/include/openmc/constants.h +++ b/include/openmc/constants.h @@ -366,7 +366,7 @@ enum class SolverType { MONTE_CARLO, RANDOM_RAY }; enum class RandomRayVolumeEstimator { NAIVE, SIMULATION_AVERAGED, HYBRID }; enum class RandomRaySourceShape { FLAT, LINEAR, LINEAR_XY }; -enum class RandomRaySampleMethod { PRNG, HALTON }; +enum class RandomRaySampleMethod { PRNG, HALTON, S2 }; //============================================================================== // Geometry Constants diff --git a/include/openmc/random_ray/random_ray.h b/include/openmc/random_ray/random_ray.h index 7fd68b83e0..b61d2d67aa 100644 --- a/include/openmc/random_ray/random_ray.h +++ b/include/openmc/random_ray/random_ray.h @@ -41,6 +41,7 @@ public: uint64_t transport_history_based_single_ray(); SourceSite sample_prng(); SourceSite sample_halton(); + SourceSite sample_s2(); //---------------------------------------------------------------------------- // Static data members diff --git a/openmc/settings.py b/openmc/settings.py index 3cf662e311..2ad1d06e75 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -207,7 +207,11 @@ class Settings: default is 'False'. :sample_method: Sampling method for the ray starting location and direction of - travel. Options are `prng` (default) or 'halton`. + travel. Options are `prng` (default), `halton`, or `s2`. `s2` + modifies the `prng` sampling method such that rays are sampled + with directions (-1, 0, 0) or (1, 0, 0). This is used for verification + against analytic transport benchmarks which are often derivied with + a reduced angular domain. :source_region_meshes: List of tuples where each tuple contains a mesh and a list of domains. Each domain is an instance of openmc.Material, openmc.Cell, @@ -1351,7 +1355,7 @@ class Settings: 'openmc.Material, openmc.Cell, or openmc.Universe.') elif key == 'sample_method': cv.check_value('sample method', value, - ('prng', 'halton')) + ('prng', 'halton', 's2')) elif key == 'diagonal_stabilization_rho': cv.check_type('diagonal stabilization rho', value, Real) cv.check_greater_than('diagonal stabilization rho', diff --git a/src/random_ray/random_ray.cpp b/src/random_ray/random_ray.cpp index 44e161a8e6..821c5d1ba9 100644 --- a/src/random_ray/random_ray.cpp +++ b/src/random_ray/random_ray.cpp @@ -792,6 +792,9 @@ void RandomRay::initialize_ray(uint64_t ray_id, FlatSourceDomain* domain) case RandomRaySampleMethod::HALTON: site = sample_halton(); break; + case RandomRaySampleMethod::S2: + site = sample_s2(); + break; default: fatal_error("Unknown sample method for random ray transport."); } @@ -874,4 +877,27 @@ SourceSite RandomRay::sample_halton() return site; } +SourceSite RandomRay::sample_s2() +{ + // set random number seed + int64_t particle_seed = + (simulation::current_batch - 1) * settings::n_particles + id(); + init_particle_seeds(particle_seed, seeds()); + stream() = STREAM_TRACKING; + + // Get spatial component of the ray_source_ + SpatialDistribution* space = + dynamic_cast(RandomRay::ray_source_.get())->space(); + + SourceSite site; + + // Sample spatial distribution + site.r = space->sample(current_seed()).first; + + // Sample either left or right for S2 (flashlight) transport. + site.u = {prn(current_seed()) < 0.5 ? -1 : 1, 0.0, 0.0}; + + return site; +} + } // namespace openmc diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index 5970dc65a2..80cbfc3fe5 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -576,9 +576,18 @@ void RandomRaySimulation::print_results_random_ray( fatal_error("Invalid random ray source shape"); } fmt::print(" Source Shape = {}\n", shape); - std::string sample_method = - (RandomRay::sample_method_ == RandomRaySampleMethod::PRNG) ? "PRNG" - : "Halton"; + std::string sample_method; + switch (RandomRay::sample_method_) { + case RandomRaySampleMethod::PRNG: + sample_method = "PRNG"; + break; + case RandomRaySampleMethod::HALTON: + sample_method = "Halton"; + break; + case RandomRaySampleMethod::S2: + sample_method = "PRNG S2"; + break; + } fmt::print(" Sample Method = {}\n", sample_method); if (domain_->is_transport_stabilization_needed_) { diff --git a/src/settings.cpp b/src/settings.cpp index 5b472468fc..4b356d028f 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -326,6 +326,8 @@ void get_run_parameters(pugi::xml_node node_base) RandomRay::sample_method_ = RandomRaySampleMethod::PRNG; } else if (temp_str == "halton") { RandomRay::sample_method_ = RandomRaySampleMethod::HALTON; + } else if (temp_str == "s2") { + RandomRay::sample_method_ = RandomRaySampleMethod::S2; } else { fatal_error("Unrecognized sample method: " + temp_str); } diff --git a/tests/regression_tests/random_ray_s2/__init__.py b/tests/regression_tests/random_ray_s2/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regression_tests/random_ray_s2/inputs_true.dat b/tests/regression_tests/random_ray_s2/inputs_true.dat new file mode 100644 index 0000000000..aad8ea28b8 --- /dev/null +++ b/tests/regression_tests/random_ray_s2/inputs_true.dat @@ -0,0 +1,69 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + fixed source + 100 + 30 + 10 + + + 0.0 -5.0 -5.0 40.0 5.0 5.0 + + + 1000.0 1.0 + + + material + 1 + + + multi-group + + 100.0 + 400.0 + + + 0.0 -5.0 -5.0 40.0 5.0 5.0 + + + flat + s2 + + + + + + + + 10 1 1 + 0.0 -5.0 -5.0 + 40.0 5.0 5.0 + + + + + 1 + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_s2/results_true.dat b/tests/regression_tests/random_ray_s2/results_true.dat new file mode 100644 index 0000000000..5fb6f79041 --- /dev/null +++ b/tests/regression_tests/random_ray_s2/results_true.dat @@ -0,0 +1,21 @@ +tally 1: +1.153280E+01 +6.650271E+00 +1.413926E+01 +9.995935E+00 +1.579543E+01 +1.247479E+01 +1.676986E+01 +1.406141E+01 +1.722054E+01 +1.482734E+01 +1.722054E+01 +1.482734E+01 +1.676986E+01 +1.406141E+01 +1.579543E+01 +1.247479E+01 +1.413926E+01 +9.995935E+00 +1.153280E+01 +6.650271E+00 diff --git a/tests/regression_tests/random_ray_s2/test.py b/tests/regression_tests/random_ray_s2/test.py new file mode 100644 index 0000000000..712d9c1241 --- /dev/null +++ b/tests/regression_tests/random_ray_s2/test.py @@ -0,0 +1,82 @@ +import os + +import openmc +import openmc.model +import numpy as np + +from tests.testing_harness import TolerantPyAPITestHarness + + +class MGXSTestHarness(TolerantPyAPITestHarness): + def _cleanup(self): + super()._cleanup() + f = 'mgxs.h5' + if os.path.exists(f): + os.remove(f) + + +def test_random_ray_s2(): + NUM_SOURCE_REGIONS = 10 + L = 40.0 + + # Make the simple MGXS library and material. + groups = openmc.mgxs.EnergyGroups(group_edges=[1e-5, 20.0e6]) + + domain_mat_data = openmc.XSdata('domain', groups) + domain_mat_data.order = 0 + domain_mat_data.set_total([0.1]) + domain_mat_data.set_absorption([0.1]) + domain_mat_data.set_scatter_matrix(np.rollaxis(np.array([[[0.0]]]), 0, 3)) + mg_cross_sections_file = openmc.MGXSLibrary(groups) + mg_cross_sections_file.add_xsdatas([domain_mat_data]) + mg_cross_sections_file.export_to_hdf5() + + domain_data = openmc.Macroscopic('domain') + domain_mat = openmc.Material(name='domain') + domain_mat.set_density('macro', 1.0) + domain_mat.add_macroscopic(domain_data) + + container = openmc.model.RectangularPrism(width=10.0, height=10.0, axis='x', + origin=(0.0, 0.0), boundary_type='reflective') + left = openmc.XPlane(x0 = 0.0, boundary_type='vacuum') + right = openmc.XPlane(x0 = L, boundary_type='vacuum') + cell = [openmc.Cell(region = +left & -right & -container, fill = domain_mat)] + + model = openmc.model.Model() + model.geometry = openmc.Geometry(root=openmc.Universe(cells=cell)) + model.materials = openmc.Materials([domain_mat]) + model.materials.cross_sections = './mgxs.h5' + + mesh = openmc.RegularMesh() + mesh.dimension = (NUM_SOURCE_REGIONS, 1, 1) + mesh.lower_left = (0.0, -5.0, -5.0) + mesh.upper_right = (L, 5.0, 5.0) + + tally = openmc.Tally(name="LR") + tally.filters = [openmc.MeshFilter(mesh)] + tally.scores = ['flux'] + tally.estimator = 'tracklength' + model.tallies.append(tally) + + uniform_dist = openmc.stats.Box((0.0, -5.0, -5.0), (L, 5.0, 5.0)) + model.settings.source = [ + openmc.IndependentSource(space=uniform_dist, + energy=openmc.stats.Discrete(x = 1e3, p = 1.0), + constraints={'domains' : [domain_mat]}) + ] + model.settings.energy_mode = "multi-group" + model.settings.batches = 30 + model.settings.inactive = 10 + model.settings.particles = 100 + model.settings.run_mode = 'fixed source' + model.settings.random_ray['distance_inactive'] = 100.0 + model.settings.random_ray['distance_active'] = 400.0 + model.settings.random_ray['ray_source'] = openmc.IndependentSource(space=uniform_dist) + model.settings.random_ray['source_shape'] = 'flat' + model.settings.random_ray['sample_method'] = 's2' + model.settings.random_ray['source_region_meshes'] = [(mesh, [model.geometry.root_universe])] + + model.export_to_model_xml() + + harness = MGXSTestHarness('statepoint.30.h5', model) + harness.main() From efefdb17b1a0d0856278e10761683cae8694bc35 Mon Sep 17 00:00:00 2001 From: Kevin Sawatzky <66632997+nuclearkevin@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:03:19 -0600 Subject: [PATCH 06/38] Add more operator overloads in the new Tensor class (#3822) --- include/openmc/tensor.h | 25 +++++++++++++++++++++++++ tests/cpp_unit_tests/test_tensor.cpp | 23 ++++++++++++++++++++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/include/openmc/tensor.h b/include/openmc/tensor.h index 9d428aaebd..9e443e72f9 100644 --- a/include/openmc/tensor.h +++ b/include/openmc/tensor.h @@ -762,6 +762,24 @@ public: data_[i] += o.data_[i]; return *this; } + Tensor& operator-=(const Tensor& o) + { + for (size_t i = 0; i < data_.size(); ++i) + data_[i] -= o.data_[i]; + return *this; + } + Tensor& operator*=(const Tensor& o) + { + for (size_t i = 0; i < data_.size(); ++i) + data_[i] *= o.data_[i]; + return *this; + } + Tensor& operator/=(const Tensor& o) + { + for (size_t i = 0; i < data_.size(); ++i) + data_[i] /= o.data_[i]; + return *this; + } Tensor operator+(const Tensor& o) const { @@ -784,6 +802,13 @@ public: r.data_[i] = data_[i] / o.data_[i]; return r; } + Tensor operator*(const Tensor& o) const + { + Tensor r(shape_); + for (size_t i = 0; i < data_.size(); ++i) + r.data_[i] = data_[i] * o.data_[i]; + return r; + } Tensor operator+(T val) const { diff --git a/tests/cpp_unit_tests/test_tensor.cpp b/tests/cpp_unit_tests/test_tensor.cpp index 6eae1cea6a..0936d866a9 100644 --- a/tests/cpp_unit_tests/test_tensor.cpp +++ b/tests/cpp_unit_tests/test_tensor.cpp @@ -350,6 +350,9 @@ TEST_CASE("Tensor element-wise arithmetic") c = a / b; REQUIRE(c[0] == 0.25); + + c = a * b; + REQUIRE(c[0] == 4.0); } TEST_CASE("Tensor scalar arithmetic") @@ -378,7 +381,7 @@ TEST_CASE("Tensor scalar arithmetic") REQUIRE(b[0] == 11.0); } -TEST_CASE("Tensor compound addition with tensor") +TEST_CASE("Tensor compound arithmetic with tensor") { Tensor a({3}, 0.0); Tensor b({3}, 0.0); @@ -388,6 +391,24 @@ TEST_CASE("Tensor compound addition with tensor") REQUIRE(a[0] == 11.0); REQUIRE(a[1] == 22.0); REQUIRE(a[2] == 33.0); + + a = {1.0, 2.0, 3.0}; + b -= a; + REQUIRE(b[0] == 9.0); + REQUIRE(b[1] == 18.0); + REQUIRE(b[2] == 27.0); + + b = {10.0, 20.0, 30.0}; + a *= b; + REQUIRE(a[0] == 10.0); + REQUIRE(a[1] == 40.0); + REQUIRE(a[2] == 90.0); + + a = {1.0, 2.0, 3.0}; + b /= a; + REQUIRE(b[0] == 10.0); + REQUIRE(b[1] == 10.0); + REQUIRE(b[2] == 10.0); } TEST_CASE("Tensor comparison operators") From 53ce1910f9e72fed4d27ff47d50ced98827abc89 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Thu, 19 Feb 2026 12:25:31 -0600 Subject: [PATCH 07/38] Fix S2 Random Ray Casting Issue (#3825) --- src/random_ray/random_ray.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/random_ray/random_ray.cpp b/src/random_ray/random_ray.cpp index 821c5d1ba9..dde5023e44 100644 --- a/src/random_ray/random_ray.cpp +++ b/src/random_ray/random_ray.cpp @@ -895,7 +895,7 @@ SourceSite RandomRay::sample_s2() site.r = space->sample(current_seed()).first; // Sample either left or right for S2 (flashlight) transport. - site.u = {prn(current_seed()) < 0.5 ? -1 : 1, 0.0, 0.0}; + site.u = {prn(current_seed()) < 0.5 ? -1.0 : 1.0, 0.0, 0.0}; return site; } From 153281a490a55a814a7fbaae573080d447bc618e Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Sat, 21 Feb 2026 16:00:22 +0100 Subject: [PATCH 08/38] Remove unused class Tabulated2D (#3818) --- openmc/data/endf.py | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/openmc/data/endf.py b/openmc/data/endf.py index 73ae210b62..e163ad4c22 100644 --- a/openmc/data/endf.py +++ b/openmc/data/endf.py @@ -225,24 +225,3 @@ class Evaluation: self.target['mass_number'], self.target['isomeric_state']) - -class Tabulated2D: - """Metadata for a two-dimensional function. - - This is a dummy class that is not really used other than to store the - interpolation information for a two-dimensional function. Once we refactor - to adopt GNDS-like data containers, this will probably be removed or - extended. - - Parameters - ---------- - breakpoints : Iterable of int - Breakpoints for interpolation regions - interpolation : Iterable of int - Interpolation scheme identification number, e.g., 3 means y is linear in - ln(x). - - """ - def __init__(self, breakpoints, interpolation): - self.breakpoints = breakpoints - self.interpolation = interpolation From 139907c955354e97f0a74f00e0a774ff52823818 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Sat, 21 Feb 2026 10:17:12 -0600 Subject: [PATCH 09/38] Implement tally filter for filtering by reaction (#3809) --- CMakeLists.txt | 1 + docs/source/pythonapi/base.rst | 1 + docs/source/pythonapi/capi.rst | 2 + docs/source/usersguide/tallies.rst | 221 ++++++++++-------- include/openmc/constants.h | 1 + include/openmc/endf.h | 7 + include/openmc/reaction.h | 16 +- include/openmc/tallies/filter.h | 1 + include/openmc/tallies/filter_reaction.h | 52 +++++ openmc/data/reaction.py | 12 + openmc/filter.py | 83 ++++++- openmc/lib/filter.py | 17 +- src/chain.cpp | 2 +- src/endf.cpp | 77 ++++++ src/reaction.cpp | 78 +++---- src/settings.cpp | 2 +- src/tallies/filter.cpp | 3 + src/tallies/filter_reaction.cpp | 79 +++++++ src/tallies/tally.cpp | 8 +- .../filter_reaction/__init__.py | 0 .../filter_reaction/inputs_true.dat | 27 +++ .../filter_reaction/results_true.dat | 13 ++ .../regression_tests/filter_reaction/test.py | 31 +++ tests/unit_tests/test_filter_reaction.py | 87 +++++++ 24 files changed, 661 insertions(+), 160 deletions(-) create mode 100644 include/openmc/tallies/filter_reaction.h create mode 100644 src/tallies/filter_reaction.cpp create mode 100644 tests/regression_tests/filter_reaction/__init__.py create mode 100644 tests/regression_tests/filter_reaction/inputs_true.dat create mode 100644 tests/regression_tests/filter_reaction/results_true.dat create mode 100644 tests/regression_tests/filter_reaction/test.py create mode 100644 tests/unit_tests/test_filter_reaction.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 5d6dd2a0ea..5e6a647612 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -412,6 +412,7 @@ list(APPEND libopenmc_SOURCES src/tallies/filter_particle.cpp src/tallies/filter_particle_production.cpp src/tallies/filter_polar.cpp + src/tallies/filter_reaction.cpp src/tallies/filter_sph_harm.cpp src/tallies/filter_sptl_legendre.cpp src/tallies/filter_surface.cpp diff --git a/docs/source/pythonapi/base.rst b/docs/source/pythonapi/base.rst index 7bd03f379e..b3911c8b3b 100644 --- a/docs/source/pythonapi/base.rst +++ b/docs/source/pythonapi/base.rst @@ -149,6 +149,7 @@ Constructing Tallies openmc.ZernikeRadialFilter openmc.ParentNuclideFilter openmc.ParticleFilter + openmc.ReactionFilter openmc.MeshMaterialVolumes openmc.Trigger openmc.TallyDerivative diff --git a/docs/source/pythonapi/capi.rst b/docs/source/pythonapi/capi.rst index 465df14d86..dab45481dd 100644 --- a/docs/source/pythonapi/capi.rst +++ b/docs/source/pythonapi/capi.rst @@ -82,7 +82,9 @@ Classes Nuclide ParentNuclideFilter ParticleFilter + ParticleProductionFilter PolarFilter + ReactionFilter RectilinearMesh RegularMesh SpatialLegendreFilter diff --git a/docs/source/usersguide/tallies.rst b/docs/source/usersguide/tallies.rst index e3b4e508bc..5b5630556a 100644 --- a/docs/source/usersguide/tallies.rst +++ b/docs/source/usersguide/tallies.rst @@ -105,109 +105,124 @@ The following tables show all valid scores: .. table:: **Reaction scores: units are reactions per source particle.** - +----------------------+---------------------------------------------------+ - |Score | Description | - +======================+===================================================+ - |absorption |Total absorption rate. For incident neutrons, this | - | |accounts for all reactions that do not produce | - | |secondary neutrons as well as fission. For incident| - | |photons, this includes photoelectric and pair | - | |production. | - +----------------------+---------------------------------------------------+ - |elastic |Elastic scattering reaction rate. | - +----------------------+---------------------------------------------------+ - |fission |Total fission reaction rate. | - +----------------------+---------------------------------------------------+ - |scatter |Total scattering rate. | - +----------------------+---------------------------------------------------+ - |total |Total reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,2nd) |(n,2nd) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,2n) |(n,2n) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,3n) |(n,3n) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,na) |(n,n\ :math:`\alpha`\ ) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,n3a) |(n,n3\ :math:`\alpha`\ ) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,2na) |(n,2n\ :math:`\alpha`\ ) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,3na) |(n,3n\ :math:`\alpha`\ ) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,np) |(n,np) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,n2a) |(n,n2\ :math:`\alpha`\ ) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,2n2a) |(n,2n2\ :math:`\alpha`\ ) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,nd) |(n,nd) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,nt) |(n,nt) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,n3He) |(n,n\ :sup:`3`\ He) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,nd2a) |(n,nd2\ :math:`\alpha`\ ) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,nt2a) |(n,nt2\ :math:`\alpha`\ ) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,4n) |(n,4n) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,2np) |(n,2np) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,3np) |(n,3np) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,n2p) |(n,n2p) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,n*X*) |Level inelastic scattering reaction rate. The *X* | - | |indicates what which inelastic level, e.g., (n,n3) | - | |is third-level inelastic scattering. | - +----------------------+---------------------------------------------------+ - |(n,nc) |Continuum level inelastic scattering reaction rate.| - +----------------------+---------------------------------------------------+ - |(n,gamma) |Radiative capture reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,p) |(n,p) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,d) |(n,d) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,t) |(n,t) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,3He) |(n,\ :sup:`3`\ He) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,a) |(n,\ :math:`\alpha`\ ) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,2a) |(n,2\ :math:`\alpha`\ ) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,3a) |(n,3\ :math:`\alpha`\ ) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,2p) |(n,2p) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,pa) |(n,p\ :math:`\alpha`\ ) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,t2a) |(n,t2\ :math:`\alpha`\ ) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,d2a) |(n,d2\ :math:`\alpha`\ ) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,pd) |(n,pd) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,pt) |(n,pt) reaction rate. | - +----------------------+---------------------------------------------------+ - |(n,da) |(n,d\ :math:`\alpha`\ ) reaction rate. | - +----------------------+---------------------------------------------------+ - |coherent-scatter |Coherent (Rayleigh) scattering reaction rate. | - +----------------------+---------------------------------------------------+ - |incoherent-scatter |Incoherent (Compton) scattering reaction rate. | - +----------------------+---------------------------------------------------+ - |photoelectric |Photoelectric absorption reaction rate. | - +----------------------+---------------------------------------------------+ - |pair-production |Pair production reaction rate. | - +----------------------+---------------------------------------------------+ - |*Arbitrary integer* |An arbitrary integer is interpreted to mean the | - | |reaction rate for a reaction with a given ENDF MT | - | |number. | - +----------------------+---------------------------------------------------+ + +------------------------+-------------------------------------------------+ + |Score |Description | + +========================+=================================================+ + |absorption |Total absorption rate. For incident neutrons, | + | |this accounts for all reactions that do not | + | |produce secondary neutrons as well as fission. | + | |For incident photons, this includes | + | |photoelectric and pair production. | + +------------------------+-------------------------------------------------+ + |elastic |Elastic scattering reaction rate. | + +------------------------+-------------------------------------------------+ + |fission |Total fission reaction rate. | + +------------------------+-------------------------------------------------+ + |scatter |Total scattering rate. | + +------------------------+-------------------------------------------------+ + |total |Total reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,2nd) |(n,2nd) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,2n) |(n,2n) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,3n) |(n,3n) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,na) |(n,n\ :math:`\alpha`\ ) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,n3a) |(n,n3\ :math:`\alpha`\ ) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,2na) |(n,2n\ :math:`\alpha`\ ) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,3na) |(n,3n\ :math:`\alpha`\ ) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,np) |(n,np) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,n2a) |(n,n2\ :math:`\alpha`\ ) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,2n2a) |(n,2n2\ :math:`\alpha`\ ) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,nd) |(n,nd) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,nt) |(n,nt) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,n3He) |(n,n\ :sup:`3`\ He) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,nd2a) |(n,nd2\ :math:`\alpha`\ ) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,nt2a) |(n,nt2\ :math:`\alpha`\ ) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,4n) |(n,4n) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,2np) |(n,2np) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,3np) |(n,3np) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,n2p) |(n,n2p) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,npa) |(n,np\ :math:`\alpha`\ ) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,n*X*) |Level inelastic scattering reaction rate. The | + | |*X* indicates which inelastic level, e.g., | + | |(n,n3) is third-level inelastic scattering. | + +------------------------+-------------------------------------------------+ + |(n,nc) |Continuum level inelastic scattering | + | |reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,gamma) |Radiative capture reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,p) |(n,p) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,d) |(n,d) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,t) |(n,t) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,3He) |(n,\ :sup:`3`\ He) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,a) |(n,\ :math:`\alpha`\ ) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,2a) |(n,2\ :math:`\alpha`\ ) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,3a) |(n,3\ :math:`\alpha`\ ) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,2p) |(n,2p) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,pa) |(n,p\ :math:`\alpha`\ ) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,t2a) |(n,t2\ :math:`\alpha`\ ) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,d2a) |(n,d2\ :math:`\alpha`\ ) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,pd) |(n,pd) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,pt) |(n,pt) reaction rate. | + +------------------------+-------------------------------------------------+ + |(n,da) |(n,d\ :math:`\alpha`\ ) reaction rate. | + +------------------------+-------------------------------------------------+ + |photon-total |Total photo-atomic reaction rate. | + +------------------------+-------------------------------------------------+ + |coherent-scatter |Coherent (Rayleigh) scattering reaction rate. | + +------------------------+-------------------------------------------------+ + |incoherent-scatter |Incoherent (Compton) scattering reaction rate. | + +------------------------+-------------------------------------------------+ + |photoelectric |Photoelectric absorption reaction rate. | + +------------------------+-------------------------------------------------+ + |photoelectric-*S* |Subshell photoelectric absorption rate for the | + | |*S* shell. For example, "photoelectric-N3" is the| + | |rate for the N3 subshell. | + +------------------------+-------------------------------------------------+ + |pair-production |Pair production reaction rate (total). | + +------------------------+-------------------------------------------------+ + |pair-production-electron|Pair production reaction rate in the electron | + | |field. | + +------------------------+-------------------------------------------------+ + |pair-production-nuclear |Pair production reaction rate in the nuclear | + | |field. | + +------------------------+-------------------------------------------------+ + |*Arbitrary integer* |An arbitrary integer is interpreted to mean the | + | |reaction rate for a reaction with a given ENDF | + | |MT number. | + +------------------------+-------------------------------------------------+ .. table:: **Particle production scores: units are particles produced per source particles.** diff --git a/include/openmc/constants.h b/include/openmc/constants.h index e62b2941c7..0b425a673d 100644 --- a/include/openmc/constants.h +++ b/include/openmc/constants.h @@ -227,6 +227,7 @@ enum ReactionType { N_XA = 207, HEATING = 301, DAMAGE_ENERGY = 444, + PHOTON_TOTAL = 501, COHERENT = 502, INCOHERENT = 504, PAIR_PROD_ELEC = 515, diff --git a/include/openmc/endf.h b/include/openmc/endf.h index 4a737eb881..34fee9758b 100644 --- a/include/openmc/endf.h +++ b/include/openmc/endf.h @@ -33,6 +33,13 @@ bool is_disappearance(int MT); //! \return Whether corresponding reaction is an inelastic scattering reaction bool is_inelastic_scatter(int MT); +//! Determine whether an MT number matches a target MT, considering that the +//! target may be a summation reaction. +//! \param[in] event_mt MT number of the actual event +//! \param[in] target_mt MT number to check against +//! \return Whether event_mt is a component of target_mt (or equal to it) +bool mt_matches(int event_mt, int target_mt); + //============================================================================== //! Abstract one-dimensional function //============================================================================== diff --git a/include/openmc/reaction.h b/include/openmc/reaction.h index 3314d18666..e95db1665d 100644 --- a/include/openmc/reaction.h +++ b/include/openmc/reaction.h @@ -76,11 +76,21 @@ public: //! \return Name of the corresponding reaction std::string reaction_name(int mt); -//! Return reaction type (MT value) given a reaction name +//! Return MT value for given a reaction name (including special tally MT +//! values) // //! \param[in] name Reaction name -//! \return Corresponding reaction type (MT value) -int reaction_type(std::string name); +//! \return Corresponding MT number or special tally score +int reaction_tally_mt(std::string name); + +//! Return ENDF MT number given a reaction name +// +//! Unlike reaction_tally_mt(), this function always returns a positive ENDF MT +//! number and never a special negative tally score. +//! +//! \param[in] name Reaction name +//! \return Corresponding ENDF MT number +int reaction_mt(const std::string& name); } // namespace openmc diff --git a/include/openmc/tallies/filter.h b/include/openmc/tallies/filter.h index 0fef97e502..77b0d9f420 100644 --- a/include/openmc/tallies/filter.h +++ b/include/openmc/tallies/filter.h @@ -41,6 +41,7 @@ enum class FilterType { PARTICLE, PARTICLE_PRODUCTION, POLAR, + REACTION, SPHERICAL_HARMONICS, SPATIAL_LEGENDRE, SURFACE, diff --git a/include/openmc/tallies/filter_reaction.h b/include/openmc/tallies/filter_reaction.h new file mode 100644 index 0000000000..015729eefb --- /dev/null +++ b/include/openmc/tallies/filter_reaction.h @@ -0,0 +1,52 @@ +#ifndef OPENMC_TALLIES_FILTER_REACTION_H +#define OPENMC_TALLIES_FILTER_REACTION_H + +#include "openmc/span.h" +#include "openmc/tallies/filter.h" +#include "openmc/vector.h" + +namespace openmc { + +//============================================================================== +//! Bins tally events based on the reaction type (MT number). +//============================================================================== + +class ReactionFilter : public Filter { +public: + //---------------------------------------------------------------------------- + // Constructors, destructors + + ~ReactionFilter() = default; + + //---------------------------------------------------------------------------- + // Methods + + std::string type_str() const override { return "reaction"; } + FilterType type() const override { return FilterType::REACTION; } + + void from_xml(pugi::xml_node node) override; + + void get_all_bins(const Particle& p, TallyEstimator estimator, + FilterMatch& match) const override; + + void to_statepoint(hid_t filter_group) const override; + + std::string text_label(int bin) const override; + + //---------------------------------------------------------------------------- + // Accessors + + const vector& bins() const { return bins_; } + void set_bins(span bins); + +protected: + //---------------------------------------------------------------------------- + // Data members + + //! MT numbers to match + vector bins_; +}; + +} // namespace openmc + +#endif // OPENMC_TALLIES_FILTER_REACTION_H diff --git a/openmc/data/reaction.py b/openmc/data/reaction.py index 65b59582cf..ab1285b721 100644 --- a/openmc/data/reaction.py +++ b/openmc/data/reaction.py @@ -21,6 +21,7 @@ from .function import Tabulated1D, Polynomial from .kalbach_mann import KalbachMann from .laboratory import LaboratoryAngleEnergy from .nbody import NBodyPhaseSpace +from .photon import _SUBSHELLS from .product import Product from .uncorrelated import UncorrelatedAngleEnergy @@ -54,6 +55,10 @@ REACTION_NAME = {1: '(n,total)', 2: '(n,elastic)', 3: "(n,nonelastic)", 198: '(n,n3p)', 199: '(n,3n2pa)', 200: '(n,5n2p)', 203: '(n,Xp)', 204: '(n,Xd)', 205: '(n,Xt)', 206: '(n,X3He)', 207: '(n,Xa)', 301: 'heating', 444: 'damage-energy', + 501: 'photon-total', 502: 'coherent-scatter', + 504: 'incoherent-scatter', 515: 'pair-production-electron', + 516: 'pair-production', 517: 'pair-production-nuclear', + 522: 'photoelectric', 649: '(n,pc)', 699: '(n,dc)', 749: '(n,tc)', 799: '(n,3Hec)', 849: '(n,ac)', 891: '(n,2nc)', 901: 'heating-local'} REACTION_NAME.update({i: f'(n,n{i - 50})' for i in range(51, 91)}) @@ -63,9 +68,16 @@ REACTION_NAME.update({i: f'(n,t{i - 700})' for i in range(700, 749)}) REACTION_NAME.update({i: f'(n,3He{i - 750})' for i in range(750, 799)}) REACTION_NAME.update({i: f'(n,a{i - 800})' for i in range(800, 849)}) REACTION_NAME.update({i: f'(n,2n{i - 875})' for i in range(875, 891)}) +REACTION_NAME.update( + {534 + i: f'photoelectric-{shell}' for i, shell in enumerate(_SUBSHELLS[1:])} +) REACTION_MT = {name: mt for mt, name in REACTION_NAME.items()} +REACTION_MT['total'] = 1 +REACTION_MT['elastic'] = 2 REACTION_MT['fission'] = 18 +REACTION_MT['absorption'] = 27 +REACTION_MT['capture'] = 102 FISSION_MTS = (18, 19, 20, 21, 38) diff --git a/openmc/filter.py b/openmc/filter.py index 9a2836e57c..8fa2e7e626 100644 --- a/openmc/filter.py +++ b/openmc/filter.py @@ -13,6 +13,7 @@ import pandas as pd import openmc import openmc.checkvalue as cv from .cell import Cell +from .data.reaction import REACTION_NAME, REACTION_MT from .material import Material from .mixin import IDManagerMixin from .surface import Surface @@ -22,11 +23,11 @@ from ._xml import get_elem_list, get_text _FILTER_TYPES = ( 'universe', 'material', 'cell', 'cellborn', 'surface', 'mesh', 'energy', - 'energyout', 'mu', 'musurface', 'polar', 'azimuthal', 'distribcell', 'delayedgroup', - 'energyfunction', 'cellfrom', 'materialfrom', 'legendre', 'spatiallegendre', - 'sphericalharmonics', 'zernike', 'zernikeradial', 'particle', + 'energyout', 'mu', 'musurface', 'polar', 'azimuthal', 'distribcell', + 'delayedgroup', 'energyfunction', 'cellfrom', 'materialfrom', 'legendre', + 'spatiallegendre', 'sphericalharmonics', 'zernike', 'zernikeradial', 'particle', 'particleproduction', 'cellinstance', 'collision', 'time', 'parentnuclide', - 'weight', 'meshborn', 'meshsurface', 'meshmaterial', + 'weight', 'meshborn', 'meshsurface', 'meshmaterial', 'reaction', ) _CURRENT_NAMES = ( @@ -1415,6 +1416,80 @@ class CollisionFilter(Filter): cv.check_greater_than('filter value', x, 0, equality=True) +class ReactionFilter(Filter): + """Bins tally events based on the reaction type (MT number). + + .. versionadded:: 0.15.4 + + Parameters + ---------- + bins : str, int, or iterable thereof + The reaction types to tally. Can be reaction name strings + (e.g., ``'(n,elastic)'``, ``'(n,gamma)'``) or integer MT numbers + (e.g., 2, 102). Integer MT values are automatically converted to their + canonical string representation. + filter_id : int + Unique identifier for the filter + + Attributes + ---------- + bins : numpy.ndarray of str + Reaction name strings + id : int + Unique identifier for the filter + num_bins : int + The number of filter bins + + """ + + def __init__(self, bins, filter_id=None): + self.bins = bins + self.id = filter_id + + @Filter.bins.setter + def bins(self, bins): + if isinstance(bins, (str, Integral)): + bins = [bins] + elif not isinstance(bins, list): + bins = list(bins) + normalized = [] + for b in bins: + if isinstance(b, Integral): + if int(b) not in REACTION_NAME: + raise ValueError(f"No known reaction for MT={b}") + normalized.append(REACTION_NAME[int(b)]) + elif isinstance(b, str): + if b == 'total': + warnings.warn( + "The reaction name 'total' is ambiguous. Use " + "'(n,total)' for neutron total cross section or " + "'photon-total' for photon total. Interpreting as" + "'(n,total)'.") + if b not in REACTION_MT: + raise ValueError(f"Unknown reaction name '{b}'") + normalized.append(REACTION_NAME[REACTION_MT[b]]) + else: + raise TypeError(f"Expected str or int for reaction filter " + f"bin, got {type(b)}") + self._bins = np.array(normalized, dtype=str) + + @classmethod + def from_hdf5(cls, group, **kwargs): + if group['type'][()].decode() != cls.short_name.lower(): + raise ValueError("Expected HDF5 data for filter type '" + + cls.short_name.lower() + "' but got '" + + group['type'][()].decode() + "' instead") + bins = [b.decode() for b in group['bins'][()]] + filter_id = int(group.name.split('/')[-1].lstrip('filter ')) + return cls(bins, filter_id=filter_id) + + @classmethod + def from_xml_element(cls, elem, **kwargs): + filter_id = int(get_text(elem, "id")) + bins = get_elem_list(elem, "bins", str) or [] + return cls(bins, filter_id=filter_id) + + class RealFilter(Filter): """Tally modifier that describes phase-space and other characteristics diff --git a/openmc/lib/filter.py b/openmc/lib/filter.py index dc81c3487b..574a37443a 100644 --- a/openmc/lib/filter.py +++ b/openmc/lib/filter.py @@ -22,9 +22,10 @@ __all__ = [ 'EnergyFilter', 'EnergyoutFilter', 'EnergyFunctionFilter', 'LegendreFilter', 'MaterialFilter', 'MaterialFromFilter', 'MeshFilter', 'MeshBornFilter', 'MeshMaterialFilter', 'MeshSurfaceFilter', 'MuFilter', 'MuSurfaceFilter', - 'ParentNuclideFilter', 'ParticleFilter', 'PolarFilter', 'SphericalHarmonicsFilter', - 'SpatialLegendreFilter', 'SurfaceFilter', 'TimeFilter', 'UniverseFilter', - 'WeightFilter', 'ZernikeFilter', 'ZernikeRadialFilter', 'filters' + 'ParentNuclideFilter', 'ParticleFilter', 'ParticleProductionFilter', 'PolarFilter', + 'ReactionFilter', 'SphericalHarmonicsFilter', 'SpatialLegendreFilter', + 'SurfaceFilter', 'TimeFilter', 'UniverseFilter', 'WeightFilter', 'ZernikeFilter', + 'ZernikeRadialFilter', 'filters' ] # Tally functions @@ -607,10 +608,18 @@ class ParticleFilter(Filter): return [ParticleType(i) for i in particle_i] +class ParticleProductionFilter(Filter): + filter_type = 'particleproduction' + + class PolarFilter(Filter): filter_type = 'polar' +class ReactionFilter(Filter): + filter_type = 'reaction' + + class SphericalHarmonicsFilter(Filter): filter_type = 'sphericalharmonics' @@ -711,7 +720,9 @@ _FILTER_TYPE_MAP = { 'musurface': MuSurfaceFilter, 'parentnuclide': ParentNuclideFilter, 'particle': ParticleFilter, + 'particleproduction': ParticleProductionFilter, 'polar': PolarFilter, + 'reaction': ReactionFilter, 'sphericalharmonics': SphericalHarmonicsFilter, 'spatiallegendre': SpatialLegendreFilter, 'surface': SurfaceFilter, diff --git a/src/chain.cpp b/src/chain.cpp index a60ef12cd6..4214bc4732 100644 --- a/src/chain.cpp +++ b/src/chain.cpp @@ -42,7 +42,7 @@ ChainNuclide::ChainNuclide(pugi::xml_node node) branching_ratio = std::stod(get_node_value(reaction_node, "branching_ratio")); } - int mt = reaction_type(rx_name); + int mt = reaction_mt(rx_name); reaction_products_[mt].push_back({rx_target, branching_ratio}); } diff --git a/src/endf.cpp b/src/endf.cpp index b298ebe01f..8a9c5e48a8 100644 --- a/src/endf.cpp +++ b/src/endf.cpp @@ -87,6 +87,83 @@ bool is_inelastic_scatter(int mt) } } +bool mt_matches(int event_mt, int target_mt) +{ + // Direct match + if (event_mt == target_mt) + return true; + + // Check if event_mt is a component of target_mt summation reaction + switch (target_mt) { + case TOTAL_XS: + return event_mt == ELASTIC || mt_matches(event_mt, N_NONELASTIC); + + case N_NONELASTIC: { + static constexpr int components[] = {4, 5, 11, 16, 17, 22, 23, 24, 25, 27, + 28, 29, 30, 32, 33, 34, 35, 36, 37, 41, 42, 44, 45, 152, 153, 154, 156, + 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, + 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 183, 184, 185, 186, 187, + 188, 189, 190, 194, 195, 196, 198, 199, 200}; + for (int mt : components) { + if (mt_matches(event_mt, mt)) + return true; + } + return false; + } + + case N_LEVEL: + // Inelastic scattering levels + return event_mt >= 50 && event_mt <= N_NC; + + case N_2N: + // (n,2n) to excited states + return event_mt >= N_2N0 && event_mt <= N_2NC; + + case N_FISSION: + return is_fission(event_mt); + + case 27: + return is_fission(event_mt) || is_disappearance(event_mt); + + case N_DISAPPEAR: { + return is_disappearance(event_mt); + } + + case N_P: + // (n,p) to excited states + return event_mt >= N_P0 && event_mt <= N_PC; + + case N_D: + // (n,d) to excited states + return event_mt >= N_D0 && event_mt <= N_DC; + + case N_T: + // (n,t) to excited states + return event_mt >= N_T0 && event_mt <= N_TC; + + case N_3HE: + // (n,3He) to excited states + return event_mt >= N_3HE0 && event_mt <= N_3HEC; + + case N_A: + // (n,alpha) to excited states + return event_mt >= N_A0 && event_mt <= N_AC; + + case 501: + return event_mt == 502 || event_mt == 504 || mt_matches(event_mt, 516) || + mt_matches(event_mt, 522); + + case PAIR_PROD: + return event_mt == PAIR_PROD_ELEC || event_mt == PAIR_PROD_NUC; + + case PHOTOELECTRIC: + return event_mt >= 534 && event_mt < 573; + + default: + return false; + } +} + unique_ptr read_function(hid_t group, const char* name) { hid_t obj_id = open_object(group, name); diff --git a/src/reaction.cpp b/src/reaction.cpp index 9ac5a1f528..c02e0cc407 100644 --- a/src/reaction.cpp +++ b/src/reaction.cpp @@ -309,6 +309,7 @@ std::unordered_map REACTION_NAME_MAP { {N_XA, "(n,Xa)"}, {HEATING, "heating"}, {DAMAGE_ENERGY, "damage-energy"}, + {PHOTON_TOTAL, "photon-total"}, {COHERENT, "coherent-scatter"}, {INCOHERENT, "incoherent-scatter"}, {PAIR_PROD_ELEC, "pair-production-electron"}, @@ -346,13 +347,24 @@ void initialize_maps() // Create photoelectric subshells for (int mt = 534; mt <= 572; ++mt) { REACTION_NAME_MAP[mt] = - fmt::format("photoelectric, {} subshell", SUBSHELLS[mt - 534]); + fmt::format("photoelectric-{}", SUBSHELLS[mt - 534]); } // Invert name map to create type map for (const auto& kv : REACTION_NAME_MAP) { REACTION_TYPE_MAP[kv.second] = kv.first; } + + // Alternate names + REACTION_TYPE_MAP["elastic"] = ELASTIC; + REACTION_TYPE_MAP["n2n"] = N_2N; + REACTION_TYPE_MAP["n3n"] = N_3N; + REACTION_TYPE_MAP["n4n"] = N_4N; + REACTION_TYPE_MAP["H1-production"] = N_XP; + REACTION_TYPE_MAP["H2-production"] = N_XD; + REACTION_TYPE_MAP["H3-production"] = N_XT; + REACTION_TYPE_MAP["He3-production"] = N_X3HE; + REACTION_TYPE_MAP["He4-production"] = N_XA; } std::string reaction_name(int mt) @@ -370,62 +382,42 @@ std::string reaction_name(int mt) } } -int reaction_type(std::string name) +int reaction_tally_mt(std::string name) { - // Initialize remainder of name map and all of type map + // All "total" scores should map to the special SCORE_TOTAL + if (name == "total" || name == "(n,total)" || name == "photon-total") + return SCORE_TOTAL; + + // All fission scores should map to the special SCORE_FISSION + if (name == "fission" || name == "(n,fission)") + return SCORE_FISSION; + + // Delegate everything else to reaction_mt() + return reaction_mt(name); +} + +int reaction_mt(const std::string& name) +{ + // Initialize maps if needed if (REACTION_TYPE_MAP.empty()) initialize_maps(); - // (n,total) exists in REACTION_TYPE_MAP for MT=1, but we need this to return - // the special SCORE_TOTAL score - if (name == "(n,total)") - return SCORE_TOTAL; - - // Check if type map has an entry for this reaction name + // Look up directly in type map (no score indirection) auto it = REACTION_TYPE_MAP.find(name); if (it != REACTION_TYPE_MAP.end()) { - return it->second; + int mt = it->second; + return mt; } - // Alternate names for several reactions - if (name == "elastic") { - return ELASTIC; - } else if (name == "n2n") { - return N_2N; - } else if (name == "n3n") { - return N_3N; - } else if (name == "n4n") { - return N_4N; - } else if (name == "H1-production") { - return N_XP; - } else if (name == "H2-production") { - return N_XD; - } else if (name == "H3-production") { - return N_XT; - } else if (name == "He3-production") { - return N_X3HE; - } else if (name == "He4-production") { - return N_XA; - } - - // Assume the given string is a reaction MT number. Make sure it's a natural - // number then return. + // Assume the given string is an MT number int MT = 0; try { MT = std::stoi(name); } catch (const std::invalid_argument& ex) { - throw std::invalid_argument( - "Invalid tally score \"" + name + - "\". See the docs " - "for details: " - "https://docs.openmc.org/en/stable/usersguide/tallies.html#scores"); + throw std::invalid_argument("Unknown reaction name \"" + name + "\"."); } if (MT < 1) - throw std::invalid_argument( - "Invalid tally score \"" + name + - "\". See the docs " - "for details: " - "https://docs.openmc.org/en/stable/usersguide/tallies.html#scores"); + throw std::invalid_argument("Unknown reaction name \"" + name + "\"."); return MT; } diff --git a/src/settings.cpp b/src/settings.cpp index 4b356d028f..b6a9ab9537 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -964,7 +964,7 @@ void read_settings_xml(pugi::xml_node root) if (check_for_node(node_ct, "reactions")) { auto temp = get_node_array(node_ct, "reactions"); for (const auto& b : temp) { - int reaction_int = reaction_type(b); + int reaction_int = reaction_mt(b); if (reaction_int > 0) { collision_track_config.mt_numbers.insert(reaction_int); } diff --git a/src/tallies/filter.cpp b/src/tallies/filter.cpp index b0e9624c9e..badb910773 100644 --- a/src/tallies/filter.cpp +++ b/src/tallies/filter.cpp @@ -33,6 +33,7 @@ #include "openmc/tallies/filter_particle.h" #include "openmc/tallies/filter_particle_production.h" #include "openmc/tallies/filter_polar.h" +#include "openmc/tallies/filter_reaction.h" #include "openmc/tallies/filter_sph_harm.h" #include "openmc/tallies/filter_sptl_legendre.h" #include "openmc/tallies/filter_surface.h" @@ -151,6 +152,8 @@ Filter* Filter::create(const std::string& type, int32_t id) return Filter::create(id); } else if (type == "polar") { return Filter::create(id); + } else if (type == "reaction") { + return Filter::create(id); } else if (type == "surface") { return Filter::create(id); } else if (type == "spatiallegendre") { diff --git a/src/tallies/filter_reaction.cpp b/src/tallies/filter_reaction.cpp new file mode 100644 index 0000000000..8ee9f3ce87 --- /dev/null +++ b/src/tallies/filter_reaction.cpp @@ -0,0 +1,79 @@ +#include "openmc/tallies/filter_reaction.h" + +#include + +#include "openmc/capi.h" +#include "openmc/endf.h" +#include "openmc/error.h" +#include "openmc/reaction.h" +#include "openmc/xml_interface.h" + +namespace openmc { + +//============================================================================== +// ReactionFilter implementation +//============================================================================== + +void ReactionFilter::from_xml(pugi::xml_node node) +{ + // Read bins as reaction name strings + auto bins_str = get_node_array(node, "bins"); + + // Convert reaction names to MT numbers + vector bins_mt; + bins_mt.reserve(bins_str.size()); + for (const auto& name : bins_str) { + bins_mt.push_back(reaction_mt(name)); + } + + this->set_bins(bins_mt); +} + +void ReactionFilter::set_bins(span bins) +{ + // Clear existing bins + bins_.clear(); + bins_.reserve(bins.size()); + + // Copy bins and build lookup map + for (int64_t i = 0; i < bins.size(); ++i) { + bins_.push_back(bins[i]); + } + + n_bins_ = bins_.size(); +} + +void ReactionFilter::get_all_bins( + const Particle& p, TallyEstimator estimator, FilterMatch& match) const +{ + // Get the event MT number from the particle + int event_mt = p.event_mt(); + + // Check each bin, considering summation rules + for (int64_t i = 0; i < bins_.size(); ++i) { + if (mt_matches(event_mt, bins_[i])) { + match.bins_.push_back(i); + match.weights_.push_back(1.0); + } + } +} + +void ReactionFilter::to_statepoint(hid_t filter_group) const +{ + Filter::to_statepoint(filter_group); + + // Write bins as reaction name strings for human readability + vector names; + names.reserve(bins_.size()); + for (auto mt : bins_) { + names.push_back(reaction_name(mt)); + } + write_dataset(filter_group, "bins", names); +} + +std::string ReactionFilter::text_label(int bin) const +{ + return reaction_name(bins_[bin]); +} + +} // namespace openmc diff --git a/src/tallies/tally.cpp b/src/tallies/tally.cpp index 296c3fb505..32566721f8 100644 --- a/src/tallies/tally.cpp +++ b/src/tallies/tally.cpp @@ -171,6 +171,10 @@ Tally::Tally(pugi::xml_node node) estimator_ = TallyEstimator::COLLISION; } else if (filt_type == FilterType::PARTICLE_PRODUCTION) { estimator_ = TallyEstimator::ANALOG; + } else if (filt_type == FilterType::REACTION) { + if (estimator_ == TallyEstimator::TRACKLENGTH) { + estimator_ = TallyEstimator::COLLISION; + } } } @@ -569,7 +573,7 @@ void Tally::set_scores(const vector& scores) } // Determine integer code for score - int score = reaction_type(score_str); + int score = reaction_tally_mt(score_str); switch (score) { case SCORE_FLUX: @@ -785,7 +789,7 @@ void Tally::init_triggers(pugi::xml_node node) } else { int i_score = 0; for (; i_score < this->scores_.size(); ++i_score) { - if (this->scores_[i_score] == reaction_type(score_str)) + if (this->scores_[i_score] == reaction_tally_mt(score_str)) break; } if (i_score == this->scores_.size()) { diff --git a/tests/regression_tests/filter_reaction/__init__.py b/tests/regression_tests/filter_reaction/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regression_tests/filter_reaction/inputs_true.dat b/tests/regression_tests/filter_reaction/inputs_true.dat new file mode 100644 index 0000000000..3ab9ab3fb6 --- /dev/null +++ b/tests/regression_tests/filter_reaction/inputs_true.dat @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + eigenvalue + 1000 + 5 + + + + (n,elastic) (n,2n) (n,fission) (n,gamma) (n,total) + + + 1 + flux + + + diff --git a/tests/regression_tests/filter_reaction/results_true.dat b/tests/regression_tests/filter_reaction/results_true.dat new file mode 100644 index 0000000000..61ec1c65fa --- /dev/null +++ b/tests/regression_tests/filter_reaction/results_true.dat @@ -0,0 +1,13 @@ +k-combined: +2.265297E+00 7.172807E-03 +tally 1: +8.364316E+01 +1.400120E+03 +1.412312E-01 +4.277446E-03 +5.471503E+00 +7.015849E+00 +0.000000E+00 +0.000000E+00 +1.349840E+02 +3.644648E+03 diff --git a/tests/regression_tests/filter_reaction/test.py b/tests/regression_tests/filter_reaction/test.py new file mode 100644 index 0000000000..3c60fd5173 --- /dev/null +++ b/tests/regression_tests/filter_reaction/test.py @@ -0,0 +1,31 @@ +import openmc + +from tests.testing_harness import PyAPITestHarness + + +def test_filter_reaction(): + model = openmc.Model() + + m = openmc.Material() + m.set_density('g/cm3', 10.0) + m.add_nuclide('U235', 1.0) + model.materials.append(m) + + s = openmc.Sphere(r=100.0, boundary_type='vacuum') + c = openmc.Cell(fill=m, region=-s) + model.geometry = openmc.Geometry([c]) + + # Create a tally with reaction filter + tally = openmc.Tally() + tally.filters = [openmc.ReactionFilter( + ['(n,elastic)', '(n,2n)', '(n,fission)', '(n,gamma)', 'total'] + )] + tally.scores = ['flux'] + model.tallies = openmc.Tallies([tally]) + + # Reduce particles for faster testing + model.settings.particles = 1000 + model.settings.batches = 5 + + harness = PyAPITestHarness('statepoint.5.h5', model) + harness.main() diff --git a/tests/unit_tests/test_filter_reaction.py b/tests/unit_tests/test_filter_reaction.py new file mode 100644 index 0000000000..df8fc9d7b4 --- /dev/null +++ b/tests/unit_tests/test_filter_reaction.py @@ -0,0 +1,87 @@ +import pytest +import openmc + + +def test_reaction_filter_construction_with_strings(): + f = openmc.ReactionFilter(['(n,elastic)', '(n,gamma)']) + assert len(f.bins) == 2 + assert f.bins[0] == '(n,elastic)' + assert f.bins[1] == '(n,gamma)' + + +def test_reaction_filter_construction_with_mt(): + f = openmc.ReactionFilter([2, 102]) + assert len(f.bins) == 2 + assert f.bins[0] == '(n,elastic)' + assert f.bins[1] == '(n,gamma)' + + +def test_reaction_filter_mixed(): + f = openmc.ReactionFilter([2, '(n,gamma)']) + assert f.bins[0] == '(n,elastic)' + assert f.bins[1] == '(n,gamma)' + + +def test_reaction_filter_single_bin_string(): + f = openmc.ReactionFilter('(n,elastic)') + assert len(f.bins) == 1 + assert f.bins[0] == '(n,elastic)' + + +def test_reaction_filter_single_bin_mt(): + f = openmc.ReactionFilter(2) + assert len(f.bins) == 1 + assert f.bins[0] == '(n,elastic)' + + +def test_reaction_filter_single_bin_naming(): + f = openmc.ReactionFilter('total') + assert len(f.bins) == 1 + assert f.bins[0] == '(n,total)' + + +def test_reaction_filter_invalid_mt(): + with pytest.raises(ValueError, match="No known reaction"): + openmc.ReactionFilter([999999]) + + +def test_reaction_filter_invalid_string(): + with pytest.raises(ValueError, match="Unknown reaction name"): + openmc.ReactionFilter(['not-a-reaction']) + + +def test_reaction_filter_invalid_type(): + with pytest.raises(TypeError, match="Expected str or int"): + openmc.ReactionFilter([3.14]) + + +def test_reaction_filter_xml_roundtrip(): + f = openmc.ReactionFilter([2, 102], filter_id=42) + elem = f.to_xml_element() + f2 = openmc.ReactionFilter.from_xml_element(elem) + assert f2.id == 42 + assert len(f2.bins) == 2 + assert f2.bins[0] == '(n,elastic)' + assert f2.bins[1] == '(n,gamma)' + + +def test_reaction_filter_num_bins(): + f = openmc.ReactionFilter(['(n,elastic)', '(n,fission)', '(n,gamma)']) + assert f.num_bins == 3 + + +def test_reaction_filter_repr(): + f = openmc.ReactionFilter([2, 102]) + r = repr(f) + assert 'ReactionFilter' in r + + +def test_reaction_filter_short_name(): + assert openmc.ReactionFilter.short_name == 'Reaction' + + +def test_reaction_filter_total_warning(): + """Test that using 'total' emits a warning about ambiguity.""" + with pytest.warns(UserWarning, match="ambiguous"): + f = openmc.ReactionFilter(['total']) + assert f.bins[0] == '(n,total)' From 83a30f6860c79fac8fd58584489cc0d1abcb0376 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Sat, 21 Feb 2026 22:00:36 +0200 Subject: [PATCH 10/38] Support arbitrary symmetry axis for CylindricalIndependent class (#3474) Co-authored-by: Paul Romano --- docs/source/io_formats/settings.rst | 9 +++ docs/source/pythonapi/stats.rst | 1 + include/openmc/distribution_spatial.h | 14 +++- openmc/stats/multivariate.py | 101 ++++++++++++++++++++++-- src/distribution_spatial.cpp | 41 +++++++++- tests/unit_tests/test_source.py | 15 ---- tests/unit_tests/test_stats.py | 107 ++++++++++++++++++++++++++ 7 files changed, 260 insertions(+), 28 deletions(-) diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index ed7c6273aa..db72984a63 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -814,6 +814,7 @@ attributes/sub-elements: For a "cylindrical" distribution, no parameters are specified. Instead, the ``r``, ``phi``, ``z``, and ``origin`` elements must be specified. + Optionally, the ``r_dir`` and ``z_dir`` elements could be specified. For a "spherical" distribution, no parameters are specified. Instead, the ``r``, ``theta``, ``phi``, and ``origin`` elements must be specified. @@ -845,6 +846,10 @@ attributes/sub-elements: of a univariate probability distribution (see the description in :ref:`univariate`). + :r_dir: + For "cylindrical" distributions, this element specifies the direction + of the cylinder r-axis at phi=0. Defaults to (1.0, 0.0, 0.0). + :theta: For a "spherical" distribution, this element specifies the distribution of theta-coordinates. The necessary sub-elements/attributes are those of a @@ -857,6 +862,10 @@ attributes/sub-elements: sub-elements/attributes are those of a univariate probability distribution (see the description in :ref:`univariate`). + :z_dir: + For "cylindrical" distributions, this element specifies the direction + of the cylinder z-axis. Defaults to (0.0, 0.0, 1.0). + :origin: For "cylindrical and "spherical" distributions, this element specifies the coordinates for the origin of the coordinate system. diff --git a/docs/source/pythonapi/stats.rst b/docs/source/pythonapi/stats.rst index c8318ba862..e0ae74e39b 100644 --- a/docs/source/pythonapi/stats.rst +++ b/docs/source/pythonapi/stats.rst @@ -67,3 +67,4 @@ Spatial Distributions :template: myfunction.rst openmc.stats.spherical_uniform + openmc.stats.cylindrical_uniform diff --git a/include/openmc/distribution_spatial.h b/include/openmc/distribution_spatial.h index 3e31b17a94..663cf5568e 100644 --- a/include/openmc/distribution_spatial.h +++ b/include/openmc/distribution_spatial.h @@ -67,12 +67,18 @@ public: Distribution* phi() const { return phi_.get(); } Distribution* z() const { return z_.get(); } Position origin() const { return origin_; } + Direction r_dir() const { return r_dir_; } + Direction phi_dir() const { return phi_dir_; } + Direction z_dir() const { return z_dir_; } private: - UPtrDist r_; //!< Distribution of r coordinates - UPtrDist phi_; //!< Distribution of phi coordinates - UPtrDist z_; //!< Distribution of z coordinates - Position origin_; //!< Cartesian coordinates of the cylinder center + UPtrDist r_; //!< Distribution of r coordinates + UPtrDist phi_; //!< Distribution of phi coordinates + UPtrDist z_; //!< Distribution of z coordinates + Position origin_; //!< Cartesian coordinates of the cylinder center + Direction r_dir_; //!< Direction of r-axis at phi=0 + Direction phi_dir_; //!< Direction of phi-axis at phi=0 + Direction z_dir_; //!< Direction of z-axis }; //============================================================================== diff --git a/openmc/stats/multivariate.py b/openmc/stats/multivariate.py index 2057f4a499..d486970377 100644 --- a/openmc/stats/multivariate.py +++ b/openmc/stats/multivariate.py @@ -12,7 +12,7 @@ import openmc import openmc.checkvalue as cv from .._xml import get_elem_list, get_text from ..mesh import MeshBase -from .univariate import PowerLaw, Uniform, Univariate +from .univariate import PowerLaw, Uniform, Univariate, delta_function class UnitSphere(ABC): @@ -610,6 +610,10 @@ class CylindricalIndependent(Spatial): origin: Iterable of float, optional coordinates (x0, y0, z0) of the center of the cylindrical reference frame. Defaults to (0.0, 0.0, 0.0) + r_dir : Iterable of float, optional + Unit vector of the cylinder r axis at phi=0. + z_dir : Iterable of float, optional + Unit vector of the cylinder z axis direction. Attributes ---------- @@ -623,14 +627,21 @@ class CylindricalIndependent(Spatial): origin: Iterable of float, optional coordinates (x0, y0, z0) of the center of the cylindrical reference frame. Defaults to (0.0, 0.0, 0.0) + r_dir : Iterable of float, optional + Unit vector of the cylinder r axis at phi=0. + z_dir : Iterable of float, optional + Unit vector of the cylinder z axis direction. """ - def __init__(self, r, phi, z, origin=(0.0, 0.0, 0.0)): + def __init__(self, r, phi, z, origin=(0.0, 0.0, 0.0), r_dir=(1.0, 0.0, 0.0), + z_dir=(0.0, 0.0, 1.0)): self.r = r self.phi = phi self.z = z self.origin = origin + self.z_dir = z_dir + self.r_dir = r_dir @property def r(self): @@ -669,6 +680,33 @@ class CylindricalIndependent(Spatial): origin = np.asarray(origin) self._origin = origin + @property + def z_dir(self): + return self._z_dir + + @z_dir.setter + def z_dir(self, z_dir): + cv.check_type('z-axis direction', z_dir, Iterable, Real) + z_dir = np.array(z_dir) + norm = np.linalg.norm(z_dir) + cv.check_greater_than('z-axis direction magnitude', norm, 0.0) + z_dir /= norm + self._z_dir = z_dir + + @property + def r_dir(self): + return self._r_dir + + @r_dir.setter + def r_dir(self, r_dir): + cv.check_type('r-axis direction', r_dir, Iterable, Real) + r_dir = np.array(r_dir) + r_dir -= np.dot(r_dir, self.z_dir) * self.z_dir + norm = np.linalg.norm(r_dir) + cv.check_greater_than('r-axis direction magnitude', norm, 0.0) + r_dir /= norm + self._r_dir = r_dir + def to_xml_element(self): """Return XML representation of the spatial distribution @@ -683,7 +721,12 @@ class CylindricalIndependent(Spatial): element.append(self.r.to_xml_element('r')) element.append(self.phi.to_xml_element('phi')) element.append(self.z.to_xml_element('z')) - element.set("origin", ' '.join(map(str, self.origin))) + if not np.allclose(self.origin, [0., 0., 0.]): + element.set("origin", ' '.join(map(str, self.origin))) + if not np.allclose(self.r_dir, [1., 0., 0.]): + element.set("r_dir", ' '.join(map(str, self.r_dir))) + if not np.allclose(self.z_dir, [0., 0., 1.]): + element.set("z_dir", ' '.join(map(str, self.z_dir))) return element @classmethod @@ -704,8 +747,10 @@ class CylindricalIndependent(Spatial): r = Univariate.from_xml_element(elem.find('r')) phi = Univariate.from_xml_element(elem.find('phi')) z = Univariate.from_xml_element(elem.find('z')) - origin = get_elem_list(elem, "origin", float) - return cls(r, phi, z, origin=origin) + origin = get_elem_list(elem, "origin", float) or [0.0, 0.0, 0.0] + r_dir = get_elem_list(elem, "r_dir", float) or [1.0, 0.0, 0.0] + z_dir = get_elem_list(elem, "z_dir", float) or [0.0, 0.0, 1.0] + return cls(r, phi, z, origin=origin, r_dir=r_dir, z_dir=z_dir) class MeshSpatial(Spatial): @@ -1219,3 +1264,49 @@ def spherical_uniform( phis_dist = Uniform(phis[0], phis[1]) return SphericalIndependent(r_dist, cos_thetas_dist, phis_dist, origin) + + +def cylindrical_uniform( + r_outer: float, + height: float, + r_inner: float = 0.0, + phis: Sequence[float] = (0., 2*pi), + **kwargs, +): + """Return a uniform spatial distribution over a cylindrical shell. + + This function provides a uniform spatial distribution over a cylindrical + shell between `r_inner` and `r_outer`. When `height` is zero, a delta + function is used for the z-distribution, giving a uniform distribution over + a flat ring (annulus) at z=0 in the local coordinate frame. Optionally, the + range of angles can be restricted by the `phis` argument. + + .. versionadded:: 0.15.4 + + Parameters + ---------- + r_outer : float + Outer radius of the cylindrical shell in [cm] + height : float + Height of the cylindrical shell in [cm]. When 0, the distribution is a + flat ring at z=0 in the local frame. + r_inner : float + Inner radius of the cylindrical shell in [cm] + phis : iterable of float + Starting and ending phi coordinates (azimuthal angle) in radians in a + reference frame centered at `origin`. + **kwargs + Keyword arguments passed directly to + :class:`~openmc.stats.CylindricalIndependent` (e.g., ``origin``, + ``r_dir``, ``z_dir``). + + Returns + ------- + openmc.stats.CylindricalIndependent + Uniform distribution over the cylindrical shell + """ + + r_dist = PowerLaw(r_inner, r_outer, 1) + phis_dist = Uniform(phis[0], phis[1]) + z_dist = delta_function(0.0) if height == 0.0 else Uniform(-height/2, height/2) + return CylindricalIndependent(r_dist, phis_dist, z_dist, **kwargs) diff --git a/src/distribution_spatial.cpp b/src/distribution_spatial.cpp index e25e08d748..ba8658f10e 100644 --- a/src/distribution_spatial.cpp +++ b/src/distribution_spatial.cpp @@ -141,6 +141,41 @@ CylindricalIndependent::CylindricalIndependent(pugi::xml_node node) // If no coordinates were specified, default to (0, 0, 0) origin_ = {0.0, 0.0, 0.0}; } + + // Read cylinder z_dir + if (check_for_node(node, "z_dir")) { + auto z_dir = get_node_array(node, "z_dir"); + if (z_dir.size() == 3) { + z_dir_ = z_dir; + z_dir_ /= z_dir_.norm(); + } else { + fatal_error("z_dir for cylindrical source distribution must be length 3"); + } + } else { + // If no z_dir was specified, default to (0, 0, 1) + z_dir_ = {0.0, 0.0, 1.0}; + } + + // Read cylinder r_dir + if (check_for_node(node, "r_dir")) { + auto r_dir = get_node_array(node, "r_dir"); + if (r_dir.size() == 3) { + r_dir_ = r_dir; + r_dir_ /= r_dir_.norm(); + } else { + fatal_error("r_dir for cylindrical source distribution must be length 3"); + } + } else { + // If no r_dir was specified, default to (1, 0, 0) + r_dir_ = {1.0, 0.0, 0.0}; + } + + if (r_dir_.dot(z_dir_) > 1e-12) + fatal_error("r_dir must be perpendicular to z_dir"); + + auto phi_dir = z_dir_.cross(r_dir_); + phi_dir /= phi_dir.norm(); + phi_dir_ = phi_dir; } std::pair CylindricalIndependent::sample(uint64_t* seed) const @@ -148,10 +183,8 @@ std::pair CylindricalIndependent::sample(uint64_t* seed) const auto [r, r_wgt] = r_->sample(seed); auto [phi, phi_wgt] = phi_->sample(seed); auto [z, z_wgt] = z_->sample(seed); - double x = r * cos(phi) + origin_.x; - double y = r * sin(phi) + origin_.y; - z += origin_.z; - Position xi {x, y, z}; + Position xi = + r * (cos(phi) * r_dir_ + sin(phi) * phi_dir_) + z * z_dir_ + origin_; return {xi, r_wgt * phi_wgt * z_wgt}; } diff --git a/tests/unit_tests/test_source.py b/tests/unit_tests/test_source.py index bb8a1b7852..6eb03f3881 100644 --- a/tests/unit_tests/test_source.py +++ b/tests/unit_tests/test_source.py @@ -35,21 +35,6 @@ def test_source(): assert src.strength == 1.0 -def test_spherical_uniform(): - r_outer = 2.0 - r_inner = 1.0 - thetas = (0.0, pi/2) - phis = (0.0, pi) - origin = (0.0, 1.0, 2.0) - - sph_indep_function = openmc.stats.spherical_uniform(r_outer, - r_inner, - thetas, - phis, - origin) - - assert isinstance(sph_indep_function, openmc.stats.SphericalIndependent) - def test_point_cloud(): positions = [(1, 0, 2), (0, 1, 0), (0, 0, 3), (4, 9, 2)] strengths = [1, 2, 3, 4] diff --git a/tests/unit_tests/test_stats.py b/tests/unit_tests/test_stats.py index b9d9c4e1fb..71679024fd 100644 --- a/tests/unit_tests/test_stats.py +++ b/tests/unit_tests/test_stats.py @@ -560,6 +560,113 @@ def test_point(): assert d.xyz == pytest.approx(p) +def test_spherical_uniform(): + r_outer = 2.0 + r_inner = 1.0 + thetas = (0.0, pi/2) + phis = (0.0, pi) + origin = (0.0, 1.0, 2.0) + + sph_indep_function = openmc.stats.spherical_uniform(r_outer, + r_inner, + thetas, + phis, + origin) + + assert isinstance(sph_indep_function, openmc.stats.SphericalIndependent) + + +def test_cylindrical_uniform(): + r_outer = 2.0 + r_inner = 1.0 + height = 1.0 + phis = (0.0, pi) + origin = (0.0, 1.0, 2.0) + + dist = openmc.stats.cylindrical_uniform(r_outer, height, r_inner, phis, + origin=origin) + + assert isinstance(dist, openmc.stats.CylindricalIndependent) + + # Check r distribution (PowerLaw with exponent 1 for uniform area sampling) + assert isinstance(dist.r, openmc.stats.PowerLaw) + assert dist.r.a == pytest.approx(r_inner) + assert dist.r.b == pytest.approx(r_outer) + assert dist.r.n == pytest.approx(1.0) + + # Check phi distribution + assert isinstance(dist.phi, openmc.stats.Uniform) + assert dist.phi.a == pytest.approx(phis[0]) + assert dist.phi.b == pytest.approx(phis[1]) + + # Check z distribution (centered on origin along z_dir) + assert isinstance(dist.z, openmc.stats.Uniform) + assert dist.z.a == pytest.approx(-height / 2) + assert dist.z.b == pytest.approx(height / 2) + + # Check origin and default directions + np.testing.assert_allclose(dist.origin, origin) + np.testing.assert_allclose(dist.r_dir, [1., 0., 0.]) + np.testing.assert_allclose(dist.z_dir, [0., 0., 1.]) + + # XML round-trip preserves all parameters + elem = dist.to_xml_element() + dist2 = openmc.stats.CylindricalIndependent.from_xml_element(elem) + np.testing.assert_allclose(dist2.origin, origin) + np.testing.assert_allclose(dist2.r_dir, dist.r_dir) + np.testing.assert_allclose(dist2.z_dir, dist.z_dir) + + +def test_cylindrical_uniform_tilted(): + # Test with non-default axis orientation (y-axis as cylinder axis) + dist = openmc.stats.cylindrical_uniform( + r_outer=3.0, height=2.0, r_dir=(1., 0., 0.), z_dir=(0., 1., 0.) + ) + np.testing.assert_allclose(dist.z_dir, [0., 1., 0.]) + np.testing.assert_allclose(dist.r_dir, [1., 0., 0.]) + + # XML round-trip preserves tilted directions + elem = dist.to_xml_element() + dist2 = openmc.stats.CylindricalIndependent.from_xml_element(elem) + np.testing.assert_allclose(dist2.z_dir, dist.z_dir) + np.testing.assert_allclose(dist2.r_dir, dist.r_dir) + + +def test_cylindrical_uniform_ring(): + # height=0 should produce a flat ring (delta function at z=0) + r_outer = 2.0 + r_inner = 1.0 + phis = (0.0, pi) + origin = (0.0, 1.0, 2.0) + + dist = openmc.stats.cylindrical_uniform(r_outer, 0.0, r_inner, phis, + origin=origin) + + assert isinstance(dist, openmc.stats.CylindricalIndependent) + + # Check r distribution + assert isinstance(dist.r, openmc.stats.PowerLaw) + assert dist.r.a == pytest.approx(r_inner) + assert dist.r.b == pytest.approx(r_outer) + assert dist.r.n == pytest.approx(1.0) + + # Check phi distribution + assert isinstance(dist.phi, openmc.stats.Uniform) + assert dist.phi.a == pytest.approx(phis[0]) + assert dist.phi.b == pytest.approx(phis[1]) + + # z distribution must be a delta function at 0.0 (local frame) + assert isinstance(dist.z, openmc.stats.Discrete) + assert dist.z.x[0] == pytest.approx(0.0) + + # XML round-trip + elem = dist.to_xml_element() + dist2 = openmc.stats.CylindricalIndependent.from_xml_element(elem) + np.testing.assert_allclose(dist2.origin, origin) + np.testing.assert_allclose(dist2.r_dir, dist.r_dir) + np.testing.assert_allclose(dist2.z_dir, dist.z_dir) + + @pytest.mark.flaky(reruns=1) def test_normal(): mean = 10.0 From 8c24c1c06435436709962fc344a768ab80db8269 Mon Sep 17 00:00:00 2001 From: Vitaly Mogulian <67088313+vitmog@users.noreply.github.com> Date: Tue, 24 Feb 2026 10:46:21 +0400 Subject: [PATCH 11/38] Modify the plotter ray tracing for its utilization in estimators (#3816) Co-authored-by: Paul Romano --- include/openmc/plot.h | 4 ++++ src/plot.cpp | 27 ++++++++++++++++++++++++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/include/openmc/plot.h b/include/openmc/plot.h index cd6be43b72..aa37394531 100644 --- a/include/openmc/plot.h +++ b/include/openmc/plot.h @@ -502,8 +502,12 @@ private: class Ray : public GeometryState { public: + // Initialize from location and direction Ray(Position r, Direction u) { init_from_r_u(r, u); } + // Initialize from known geometry state + Ray(const GeometryState& p) : GeometryState(p) {} + // Called at every surface intersection within the model virtual void on_intersection() = 0; diff --git a/src/plot.cpp b/src/plot.cpp index ea0555202d..5e3342dd68 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -1667,9 +1667,25 @@ void Ray::trace() // After phase one is done, we can starting tracing from cell to cell within // the model. This step can use neighbor lists to accelerate the ray tracing. - // Attempt to initialize the particle. We may have to enter a loop to move - // it up to the edge of the model. - bool inside_cell = exhaustive_find_cell(*this, settings::verbosity >= 10); + bool inside_cell; + // Check for location if the particle is already known + if (lowest_coord().cell() == C_NONE) { + // The geometry position of the particle is either unknown or outside of the + // edge of the model. + if (lowest_coord().universe() == C_NONE) { + // Attempt to initialize the particle. We may have to + // enter a loop to move it up to the edge of the model. + inside_cell = exhaustive_find_cell(*this, settings::verbosity >= 10); + } else { + // It has been already calculated that the current position is outside of + // the edge of the model. + inside_cell = false; + } + } else { + // Availability of the cell means that the particle is located inside the + // edge. + inside_cell = true; + } // Advance to the boundary of the model while (!inside_cell) { @@ -1750,6 +1766,11 @@ void Ray::trace() coord(lev).r() += boundary().distance() * coord(lev).u(); } surface() = boundary().surface(); + // Initialize last cells from the current cell, because the cell() variable + // does not contain the data for the case of a single-segment ray + for (int j = 0; j < n_coord(); ++j) { + cell_last(j) = coord(j).cell(); + } n_coord_last() = n_coord(); n_coord() = boundary().coord_level(); if (boundary().lattice_translation()[0] != 0 || From e130701f102ab9f87a7d1c40669fb3f4134bf53d Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 24 Feb 2026 01:35:23 -0600 Subject: [PATCH 12/38] Fix MeshFilter.get_pandas_dataframe to handle all mesh types (#3817) --- openmc/filter.py | 49 +- openmc/mesh.py | 33 +- openmc/mgxs/mdgxs.py | 4 +- openmc/mgxs/mgxs.py | 4 +- .../mgxs_library_condense/results_true.dat | 656 +++++++++--------- .../mgxs_library_mesh/results_true.dat | 656 +++++++++--------- tests/unit_tests/dagmc/test_lost_particles.py | 4 + tests/unit_tests/test_filter_mesh.py | 125 ++++ 8 files changed, 834 insertions(+), 697 deletions(-) diff --git a/openmc/filter.py b/openmc/filter.py index 8fa2e7e626..87aeb70c3a 100644 --- a/openmc/filter.py +++ b/openmc/filter.py @@ -972,53 +972,36 @@ class MeshFilter(Filter): Returns ------- pandas.DataFrame - A Pandas DataFrame with three columns describing the x,y,z mesh - cell indices corresponding to each filter bin. The number of rows - in the DataFrame is the same as the total number of bins in the - corresponding tally, with the filter bin appropriately tiled to map - to the corresponding tally bins. + A Pandas DataFrame with columns describing the mesh cell indices + corresponding to each filter bin. Column names depend on the mesh + type (e.g., x/y/z for RegularMesh, r/phi/z for CylindricalMesh, + r/theta/phi for SphericalMesh, or element index for + UnstructuredMesh). The number of rows in the DataFrame is the same + as the total number of bins in the corresponding tally, with the + filter bin appropriately tiled to map to the corresponding tally + bins. See also -------- Tally.get_pandas_dataframe(), CrossFilter.get_pandas_dataframe() """ - # Initialize Pandas DataFrame - df = pd.DataFrame() - # Initialize dictionary to build Pandas Multi-index column filter_dict = {} # Append mesh ID as outermost index of multi-index mesh_key = f'mesh {self.mesh.id}' - # Find mesh dimensions - use 3D indices for simplicity - n_dim = len(self.mesh.dimension) - if n_dim == 3: - nx, ny, nz = self.mesh.dimension - elif n_dim == 2: - nx, ny = self.mesh.dimension - nz = 1 - else: - nx = self.mesh.dimension - ny = nz = 1 + # Determine index base (0-based for unstructured, 1-based otherwise) + idx_start = 0 if isinstance(self.mesh, openmc.UnstructuredMesh) else 1 - # Generate multi-index sub-column for x-axis - filter_dict[mesh_key, 'x'] = _repeat_and_tile( - np.arange(1, nx + 1), stride, data_size) + # Generate a multi-index sub-column for each axis + for label, dim_size in zip(self.mesh._axis_labels, self.mesh.dimension): + filter_dict[mesh_key, label] = _repeat_and_tile( + np.arange(idx_start, idx_start + dim_size), stride, data_size) + stride *= dim_size - # Generate multi-index sub-column for y-axis - filter_dict[mesh_key, 'y'] = _repeat_and_tile( - np.arange(1, ny + 1), nx * stride, data_size) - - # Generate multi-index sub-column for z-axis - filter_dict[mesh_key, 'z'] = _repeat_and_tile( - np.arange(1, nz + 1), nx * ny * stride, data_size) - - # Initialize a Pandas DataFrame from the mesh dictionary - df = pd.concat([df, pd.DataFrame(filter_dict)]) - - return df + return pd.DataFrame(filter_dict) def to_xml_element(self): """Return XML Element representing the Filter. diff --git a/openmc/mesh.py b/openmc/mesh.py index efe1c20a16..946b90fbf6 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -236,12 +236,12 @@ class MeshBase(IDManagerMixin, ABC): self._name = name else: self._name = '' - + @property @abstractmethod def lower_left(self): pass - + @property @abstractmethod def upper_right(self): @@ -255,7 +255,7 @@ class MeshBase(IDManagerMixin, ABC): @abstractmethod def indices(self): pass - + @property @abstractmethod def n_elements(self): @@ -537,6 +537,11 @@ class StructuredMesh(MeshBase): def n_dimension(self): pass + @property + @abstractmethod + def _axis_labels(self): + pass + @property @abstractmethod def _grids(self): @@ -636,7 +641,7 @@ class StructuredMesh(MeshBase): s0 = (slice(0, -1),)*ndim + (slice(None),) s1 = (slice(1, None),)*ndim + (slice(None),) return (vertices[s0] + vertices[s1]) / 2 - + @property def n_elements(self): return np.prod(self.dimension) @@ -995,6 +1000,10 @@ class RegularMesh(StructuredMesh): else: return None + @property + def _axis_labels(self): + return ('x', 'y', 'z')[:self.n_dimension] + @property def lower_left(self): return self._lower_left @@ -1475,6 +1484,10 @@ class RectilinearMesh(StructuredMesh): def n_dimension(self): return 3 + @property + def _axis_labels(self): + return ('x', 'y', 'z') + @property def x_grid(self): return self._x_grid @@ -1709,6 +1722,10 @@ class CylindricalMesh(StructuredMesh): def n_dimension(self): return 3 + @property + def _axis_labels(self): + return ('r', 'phi', 'z') + @property def origin(self): return self._origin @@ -2156,6 +2173,10 @@ class SphericalMesh(StructuredMesh): def n_dimension(self): return 3 + @property + def _axis_labels(self): + return ('r', 'theta', 'phi') + @property def origin(self): return self._origin @@ -2671,6 +2692,10 @@ class UnstructuredMesh(MeshBase): def n_dimension(self): return 3 + @property + def _axis_labels(self): + return ('element_index',) + @property @require_statepoint_data def indices(self): diff --git a/openmc/mgxs/mdgxs.py b/openmc/mgxs/mdgxs.py index c12c1a9abe..57cc955ba2 100644 --- a/openmc/mgxs/mdgxs.py +++ b/openmc/mgxs/mdgxs.py @@ -877,8 +877,8 @@ class MDGXS(MGXS): # energy groups such that data is from fast to thermal if self.domain_type == 'mesh': mesh_str = f'mesh {self.domain.id}' - df.sort_values(by=[(mesh_str, 'x'), (mesh_str, 'y'), - (mesh_str, 'z')] + columns, inplace=True) + mesh_cols = [(mesh_str, label) for label in self.domain._axis_labels] + df.sort_values(by=mesh_cols + columns, inplace=True) else: df.sort_values(by=[self.domain_type] + columns, inplace=True) diff --git a/openmc/mgxs/mgxs.py b/openmc/mgxs/mgxs.py index 12b8f6a756..4c13c75087 100644 --- a/openmc/mgxs/mgxs.py +++ b/openmc/mgxs/mgxs.py @@ -2134,8 +2134,8 @@ class MGXS: # energy groups such that data is from fast to thermal if self.domain_type == 'mesh': mesh_str = f'mesh {self.domain.id}' - df.sort_values(by=[(mesh_str, 'x'), (mesh_str, 'y'), - (mesh_str, 'z')] + columns, inplace=True) + mesh_cols = [(mesh_str, label) for label in self.domain._axis_labels] + df.sort_values(by=mesh_cols + columns, inplace=True) else: df.sort_values(by=[self.domain_type] + columns, inplace=True) diff --git a/tests/regression_tests/mgxs_library_condense/results_true.dat b/tests/regression_tests/mgxs_library_condense/results_true.dat index 98b30932c3..023d5afa2a 100644 --- a/tests/regression_tests/mgxs_library_condense/results_true.dat +++ b/tests/regression_tests/mgxs_library_condense/results_true.dat @@ -1,189 +1,189 @@ - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.702881 0.026175 -2 1 2 1 1 total 0.706921 0.029169 -1 2 1 1 1 total 0.707809 0.024766 -3 2 2 1 1 total 0.717967 0.024008 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.431023 0.028803 -2 1 2 1 1 total 0.451864 0.030748 -1 2 1 1 1 total 0.456990 0.026359 -3 2 2 1 1 total 0.450621 0.026744 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.431023 0.028803 -2 1 2 1 1 total 0.451864 0.030748 -1 2 1 1 1 total 0.456990 0.026359 -3 2 2 1 1 total 0.450621 0.026744 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.022398 0.001401 -2 1 2 1 1 total 0.022325 0.001371 -1 2 1 1 1 total 0.022942 0.000990 -3 2 2 1 1 total 0.022705 0.001322 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.022394 0.001401 -2 1 2 1 1 total 0.022321 0.001371 -1 2 1 1 1 total 0.022935 0.000990 -3 2 2 1 1 total 0.022699 0.001322 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.011562 0.001544 -2 1 2 1 1 total 0.011852 0.001418 -1 2 1 1 1 total 0.012168 0.000958 -3 2 2 1 1 total 0.011986 0.001418 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.010836 0.000803 -2 1 2 1 1 total 0.010473 0.000591 -1 2 1 1 1 total 0.010774 0.000415 -3 2 2 1 1 total 0.010719 0.000688 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.026602 0.001957 -2 1 2 1 1 total 0.025695 0.001442 -1 2 1 1 1 total 0.026454 0.001015 -3 2 2 1 1 total 0.026310 0.001678 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 2.098256e+06 155243.612264 -2 1 2 1 1 total 2.027699e+06 114334.400924 -1 2 1 1 1 total 2.086255e+06 80325.567787 -3 2 2 1 1 total 2.075596e+06 133128.805680 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.680483 0.025407 -2 1 2 1 1 total 0.684597 0.028126 -1 2 1 1 1 total 0.684867 0.024133 -3 2 2 1 1 total 0.695262 0.023087 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.678017 0.026288 -2 1 2 1 1 total 0.674888 0.033989 -1 2 1 1 1 total 0.681736 0.025618 -3 2 2 1 1 total 0.683701 0.023788 - mesh 1 group in group out legendre nuclide mean std. dev. - x y z -0 1 1 1 1 1 P0 total 0.678017 0.026290 -1 1 1 1 1 1 P1 total 0.271858 0.011693 -2 1 1 1 1 1 P2 total 0.095219 0.002950 -3 1 1 1 1 1 P3 total 0.012808 0.004686 -8 1 2 1 1 1 P0 total 0.674888 0.033578 -9 1 2 1 1 1 P1 total 0.255058 0.009912 -10 1 2 1 1 1 P2 total 0.098001 0.005459 -11 1 2 1 1 1 P3 total 0.012058 0.005439 -4 2 1 1 1 1 P0 total 0.681736 0.025439 -5 2 1 1 1 1 P1 total 0.250820 0.009035 -6 2 1 1 1 1 P2 total 0.092563 0.006549 -7 2 1 1 1 1 P3 total 0.008511 0.003905 -12 2 2 1 1 1 P0 total 0.683701 0.024254 -13 2 2 1 1 1 P1 total 0.267345 0.011695 -14 2 2 1 1 1 P2 total 0.096222 0.005551 -15 2 2 1 1 1 P3 total 0.011515 0.003236 - mesh 1 group in group out legendre nuclide mean std. dev. - x y z -0 1 1 1 1 1 P0 total 0.678017 0.026290 -1 1 1 1 1 1 P1 total 0.271858 0.011693 -2 1 1 1 1 1 P2 total 0.095219 0.002950 -3 1 1 1 1 1 P3 total 0.012808 0.004686 -8 1 2 1 1 1 P0 total 0.674888 0.033578 -9 1 2 1 1 1 P1 total 0.255058 0.009912 -10 1 2 1 1 1 P2 total 0.098001 0.005459 -11 1 2 1 1 1 P3 total 0.012058 0.005439 -4 2 1 1 1 1 P0 total 0.681736 0.025439 -5 2 1 1 1 1 P1 total 0.250820 0.009035 -6 2 1 1 1 1 P2 total 0.092563 0.006549 -7 2 1 1 1 1 P3 total 0.008511 0.003905 -12 2 2 1 1 1 P0 total 0.683701 0.024254 -13 2 2 1 1 1 P1 total 0.267345 0.011695 -14 2 2 1 1 1 P2 total 0.096222 0.005551 -15 2 2 1 1 1 P3 total 0.011515 0.003236 - mesh 1 group in group out nuclide mean std. dev. - x y z -0 1 1 1 1 1 total 1.0 0.041785 -2 1 2 1 1 1 total 1.0 0.057717 -1 2 1 1 1 1 total 1.0 0.040074 -3 2 2 1 1 1 total 1.0 0.042758 - mesh 1 group in group out nuclide mean std. dev. - x y z -0 1 1 1 1 1 total 0.028438 0.003513 -2 1 2 1 1 1 total 0.022222 0.001560 -1 2 1 1 1 1 total 0.025698 0.002756 -3 2 2 1 1 1 total 0.026501 0.002315 - mesh 1 group in group out nuclide mean std. dev. - x y z -0 1 1 1 1 1 total 1.0 0.041785 -2 1 2 1 1 1 total 1.0 0.057717 -1 2 1 1 1 1 total 1.0 0.040074 -3 2 2 1 1 1 total 1.0 0.042758 - mesh 1 group in group out legendre nuclide mean std. dev. - x y z -0 1 1 1 1 1 P0 total 0.680483 0.038131 -1 1 1 1 1 1 P1 total 0.272847 0.016111 -2 1 1 1 1 1 P2 total 0.095565 0.004869 -3 1 1 1 1 1 P3 total 0.012854 0.004731 -8 1 2 1 1 1 P0 total 0.684597 0.048501 -9 1 2 1 1 1 P1 total 0.258727 0.016473 -10 1 2 1 1 1 P2 total 0.099411 0.007470 -11 1 2 1 1 1 P3 total 0.012231 0.005552 -4 2 1 1 1 1 P0 total 0.684867 0.036546 -5 2 1 1 1 1 P1 total 0.251972 0.013220 -6 2 1 1 1 1 P2 total 0.092988 0.007474 -7 2 1 1 1 1 P3 total 0.008550 0.003936 -12 2 2 1 1 1 P0 total 0.695262 0.037640 -13 2 2 1 1 1 P1 total 0.271866 0.016280 -14 2 2 1 1 1 P2 total 0.097849 0.006919 -15 2 2 1 1 1 P3 total 0.011710 0.003325 - mesh 1 group in group out legendre nuclide mean std. dev. - x y z -0 1 1 1 1 1 P0 total 0.680483 0.047566 -1 1 1 1 1 1 P1 total 0.272847 0.019737 -2 1 1 1 1 1 P2 total 0.095565 0.006297 -3 1 1 1 1 1 P3 total 0.012854 0.004762 -8 1 2 1 1 1 P0 total 0.684597 0.062559 -9 1 2 1 1 1 P1 total 0.258727 0.022234 -10 1 2 1 1 1 P2 total 0.099411 0.009420 -11 1 2 1 1 1 P3 total 0.012231 0.005597 -4 2 1 1 1 1 P0 total 0.684867 0.045704 -5 2 1 1 1 1 P1 total 0.251972 0.016635 -6 2 1 1 1 1 P2 total 0.092988 0.008352 -7 2 1 1 1 1 P3 total 0.008550 0.003951 -12 2 2 1 1 1 P0 total 0.695262 0.047964 -13 2 2 1 1 1 P1 total 0.271866 0.020004 -14 2 2 1 1 1 P2 total 0.097849 0.008086 -15 2 2 1 1 1 P3 total 0.011710 0.003363 - mesh 1 group out nuclide mean std. dev. - x y z -0 1 1 1 1 total 1.0 0.142430 -2 1 2 1 1 total 1.0 0.112715 -1 2 1 1 1 total 1.0 0.192385 -3 2 2 1 1 total 1.0 0.109836 - mesh 1 group out nuclide mean std. dev. - x y z -0 1 1 1 1 total 1.0 0.143959 -2 1 2 1 1 total 1.0 0.102504 -1 2 1 1 1 total 1.0 0.188381 -3 2 2 1 1 total 1.0 0.116230 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 5.060544e-07 3.651604e-08 -2 1 2 1 1 total 5.101988e-07 4.947639e-08 -1 2 1 1 1 total 5.206450e-07 2.814648e-08 -3 2 2 1 1 total 5.278759e-07 3.171554e-08 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.026414 0.001944 -2 1 2 1 1 total 0.025515 0.001432 -1 2 1 1 1 total 0.026267 0.001008 -3 2 2 1 1 total 0.026125 0.001667 - mesh 1 group in group out nuclide mean std. dev. - x y z -0 1 1 1 1 1 total 0.028220 0.003510 -2 1 2 1 1 1 total 0.021754 0.001519 -1 2 1 1 1 1 total 0.025487 0.002676 -3 2 2 1 1 1 total 0.026281 0.002263 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.702881 0.026175 +2 1 2 1 total 0.706921 0.029169 +1 2 1 1 total 0.707809 0.024766 +3 2 2 1 total 0.717967 0.024008 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.431023 0.028803 +2 1 2 1 total 0.451864 0.030748 +1 2 1 1 total 0.456990 0.026359 +3 2 2 1 total 0.450621 0.026744 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.431023 0.028803 +2 1 2 1 total 0.451864 0.030748 +1 2 1 1 total 0.456990 0.026359 +3 2 2 1 total 0.450621 0.026744 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.022398 0.001401 +2 1 2 1 total 0.022325 0.001371 +1 2 1 1 total 0.022942 0.000990 +3 2 2 1 total 0.022705 0.001322 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.022394 0.001401 +2 1 2 1 total 0.022321 0.001371 +1 2 1 1 total 0.022935 0.000990 +3 2 2 1 total 0.022699 0.001322 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.011562 0.001544 +2 1 2 1 total 0.011852 0.001418 +1 2 1 1 total 0.012168 0.000958 +3 2 2 1 total 0.011986 0.001418 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.010836 0.000803 +2 1 2 1 total 0.010473 0.000591 +1 2 1 1 total 0.010774 0.000415 +3 2 2 1 total 0.010719 0.000688 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.026602 0.001957 +2 1 2 1 total 0.025695 0.001442 +1 2 1 1 total 0.026454 0.001015 +3 2 2 1 total 0.026310 0.001678 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 2.098256e+06 155243.612264 +2 1 2 1 total 2.027699e+06 114334.400924 +1 2 1 1 total 2.086255e+06 80325.567787 +3 2 2 1 total 2.075596e+06 133128.805680 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.680483 0.025407 +2 1 2 1 total 0.684597 0.028126 +1 2 1 1 total 0.684867 0.024133 +3 2 2 1 total 0.695262 0.023087 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.678017 0.026288 +2 1 2 1 total 0.674888 0.033989 +1 2 1 1 total 0.681736 0.025618 +3 2 2 1 total 0.683701 0.023788 + mesh 1 group in group out legendre nuclide mean std. dev. + x y +0 1 1 1 1 P0 total 0.678017 0.026290 +1 1 1 1 1 P1 total 0.271858 0.011693 +2 1 1 1 1 P2 total 0.095219 0.002950 +3 1 1 1 1 P3 total 0.012808 0.004686 +8 1 2 1 1 P0 total 0.674888 0.033578 +9 1 2 1 1 P1 total 0.255058 0.009912 +10 1 2 1 1 P2 total 0.098001 0.005459 +11 1 2 1 1 P3 total 0.012058 0.005439 +4 2 1 1 1 P0 total 0.681736 0.025439 +5 2 1 1 1 P1 total 0.250820 0.009035 +6 2 1 1 1 P2 total 0.092563 0.006549 +7 2 1 1 1 P3 total 0.008511 0.003905 +12 2 2 1 1 P0 total 0.683701 0.024254 +13 2 2 1 1 P1 total 0.267345 0.011695 +14 2 2 1 1 P2 total 0.096222 0.005551 +15 2 2 1 1 P3 total 0.011515 0.003236 + mesh 1 group in group out legendre nuclide mean std. dev. + x y +0 1 1 1 1 P0 total 0.678017 0.026290 +1 1 1 1 1 P1 total 0.271858 0.011693 +2 1 1 1 1 P2 total 0.095219 0.002950 +3 1 1 1 1 P3 total 0.012808 0.004686 +8 1 2 1 1 P0 total 0.674888 0.033578 +9 1 2 1 1 P1 total 0.255058 0.009912 +10 1 2 1 1 P2 total 0.098001 0.005459 +11 1 2 1 1 P3 total 0.012058 0.005439 +4 2 1 1 1 P0 total 0.681736 0.025439 +5 2 1 1 1 P1 total 0.250820 0.009035 +6 2 1 1 1 P2 total 0.092563 0.006549 +7 2 1 1 1 P3 total 0.008511 0.003905 +12 2 2 1 1 P0 total 0.683701 0.024254 +13 2 2 1 1 P1 total 0.267345 0.011695 +14 2 2 1 1 P2 total 0.096222 0.005551 +15 2 2 1 1 P3 total 0.011515 0.003236 + mesh 1 group in group out nuclide mean std. dev. + x y +0 1 1 1 1 total 1.0 0.041785 +2 1 2 1 1 total 1.0 0.057717 +1 2 1 1 1 total 1.0 0.040074 +3 2 2 1 1 total 1.0 0.042758 + mesh 1 group in group out nuclide mean std. dev. + x y +0 1 1 1 1 total 0.028438 0.003513 +2 1 2 1 1 total 0.022222 0.001560 +1 2 1 1 1 total 0.025698 0.002756 +3 2 2 1 1 total 0.026501 0.002315 + mesh 1 group in group out nuclide mean std. dev. + x y +0 1 1 1 1 total 1.0 0.041785 +2 1 2 1 1 total 1.0 0.057717 +1 2 1 1 1 total 1.0 0.040074 +3 2 2 1 1 total 1.0 0.042758 + mesh 1 group in group out legendre nuclide mean std. dev. + x y +0 1 1 1 1 P0 total 0.680483 0.038131 +1 1 1 1 1 P1 total 0.272847 0.016111 +2 1 1 1 1 P2 total 0.095565 0.004869 +3 1 1 1 1 P3 total 0.012854 0.004731 +8 1 2 1 1 P0 total 0.684597 0.048501 +9 1 2 1 1 P1 total 0.258727 0.016473 +10 1 2 1 1 P2 total 0.099411 0.007470 +11 1 2 1 1 P3 total 0.012231 0.005552 +4 2 1 1 1 P0 total 0.684867 0.036546 +5 2 1 1 1 P1 total 0.251972 0.013220 +6 2 1 1 1 P2 total 0.092988 0.007474 +7 2 1 1 1 P3 total 0.008550 0.003936 +12 2 2 1 1 P0 total 0.695262 0.037640 +13 2 2 1 1 P1 total 0.271866 0.016280 +14 2 2 1 1 P2 total 0.097849 0.006919 +15 2 2 1 1 P3 total 0.011710 0.003325 + mesh 1 group in group out legendre nuclide mean std. dev. + x y +0 1 1 1 1 P0 total 0.680483 0.047566 +1 1 1 1 1 P1 total 0.272847 0.019737 +2 1 1 1 1 P2 total 0.095565 0.006297 +3 1 1 1 1 P3 total 0.012854 0.004762 +8 1 2 1 1 P0 total 0.684597 0.062559 +9 1 2 1 1 P1 total 0.258727 0.022234 +10 1 2 1 1 P2 total 0.099411 0.009420 +11 1 2 1 1 P3 total 0.012231 0.005597 +4 2 1 1 1 P0 total 0.684867 0.045704 +5 2 1 1 1 P1 total 0.251972 0.016635 +6 2 1 1 1 P2 total 0.092988 0.008352 +7 2 1 1 1 P3 total 0.008550 0.003951 +12 2 2 1 1 P0 total 0.695262 0.047964 +13 2 2 1 1 P1 total 0.271866 0.020004 +14 2 2 1 1 P2 total 0.097849 0.008086 +15 2 2 1 1 P3 total 0.011710 0.003363 + mesh 1 group out nuclide mean std. dev. + x y +0 1 1 1 total 1.0 0.142430 +2 1 2 1 total 1.0 0.112715 +1 2 1 1 total 1.0 0.192385 +3 2 2 1 total 1.0 0.109836 + mesh 1 group out nuclide mean std. dev. + x y +0 1 1 1 total 1.0 0.143959 +2 1 2 1 total 1.0 0.102504 +1 2 1 1 total 1.0 0.188381 +3 2 2 1 total 1.0 0.116230 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 5.060544e-07 3.651604e-08 +2 1 2 1 total 5.101988e-07 4.947639e-08 +1 2 1 1 total 5.206450e-07 2.814648e-08 +3 2 2 1 total 5.278759e-07 3.171554e-08 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.026414 0.001944 +2 1 2 1 total 0.025515 0.001432 +1 2 1 1 total 0.026267 0.001008 +3 2 2 1 total 0.026125 0.001667 + mesh 1 group in group out nuclide mean std. dev. + x y +0 1 1 1 1 total 0.028220 0.003510 +2 1 2 1 1 total 0.021754 0.001519 +1 2 1 1 1 total 0.025487 0.002676 +3 2 2 1 1 total 0.026281 0.002263 mesh 1 group in nuclide mean std. dev. x y surf 3 1 1 x-max in 1 total 4.244 0.096333 @@ -218,145 +218,145 @@ 30 2 2 y-max out 1 total 0.000 0.000000 29 2 2 y-min in 1 total 4.416 0.093648 28 2 2 y-min out 1 total 4.280 0.079070 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.943198 0.067915 -2 1 2 1 1 total 0.895741 0.058714 -1 2 1 1 1 total 0.911011 0.068461 -3 2 2 1 1 total 0.927611 0.066426 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.943198 0.067915 -2 1 2 1 1 total 0.895741 0.058714 -1 2 1 1 1 total 0.911011 0.068461 -3 2 2 1 1 total 0.927611 0.066426 - mesh 1 delayedgroup group in nuclide mean std. dev. - x y z -0 1 1 1 1 1 total 0.000006 4.453421e-07 -1 1 1 1 2 1 total 0.000032 2.303082e-06 -2 1 1 1 3 1 total 0.000031 2.202789e-06 -3 1 1 1 4 1 total 0.000072 4.961025e-06 -4 1 1 1 5 1 total 0.000032 2.071919e-06 -5 1 1 1 6 1 total 0.000013 8.662934e-07 -12 1 2 1 1 1 total 0.000006 3.282329e-07 -13 1 2 1 2 1 total 0.000031 1.701766e-06 -14 1 2 1 3 1 total 0.000030 1.629615e-06 -15 1 2 1 4 1 total 0.000070 3.675759e-06 -16 1 2 1 5 1 total 0.000031 1.536226e-06 -17 1 2 1 6 1 total 0.000013 6.423838e-07 -6 2 1 1 1 1 total 0.000006 2.308186e-07 -7 2 1 1 2 1 total 0.000032 1.211112e-06 -8 2 1 1 3 1 total 0.000031 1.169911e-06 -9 2 1 1 4 1 total 0.000072 2.685832e-06 -10 2 1 1 5 1 total 0.000032 1.187072e-06 -11 2 1 1 6 1 total 0.000013 4.939053e-07 -18 2 2 1 1 1 total 0.000006 3.819684e-07 -19 2 2 1 2 1 total 0.000032 1.976073e-06 -20 2 2 1 3 1 total 0.000031 1.889748e-06 -21 2 2 1 4 1 total 0.000072 4.252207e-06 -22 2 2 1 5 1 total 0.000032 1.765565e-06 -23 2 2 1 6 1 total 0.000013 7.386890e-07 - mesh 1 delayedgroup group out nuclide mean std. dev. - x y z -0 1 1 1 1 1 total 0.0 0.000000 -1 1 1 1 2 1 total 0.0 0.000000 -2 1 1 1 3 1 total 0.0 0.000000 -3 1 1 1 4 1 total 1.0 1.414214 -4 1 1 1 5 1 total 0.0 0.000000 -5 1 1 1 6 1 total 0.0 0.000000 -12 1 2 1 1 1 total 0.0 0.000000 -13 1 2 1 2 1 total 0.0 0.000000 -14 1 2 1 3 1 total 0.0 0.000000 -15 1 2 1 4 1 total 1.0 0.869026 -16 1 2 1 5 1 total 0.0 0.000000 -17 1 2 1 6 1 total 0.0 0.000000 -6 2 1 1 1 1 total 0.0 0.000000 -7 2 1 1 2 1 total 0.0 0.000000 -8 2 1 1 3 1 total 0.0 0.000000 -9 2 1 1 4 1 total 1.0 1.414214 -10 2 1 1 5 1 total 0.0 0.000000 -11 2 1 1 6 1 total 0.0 0.000000 -18 2 2 1 1 1 total 0.0 0.000000 -19 2 2 1 2 1 total 0.0 0.000000 -20 2 2 1 3 1 total 0.0 0.000000 -21 2 2 1 4 1 total 1.0 1.414214 -22 2 2 1 5 1 total 0.0 0.000000 -23 2 2 1 6 1 total 0.0 0.000000 - mesh 1 delayedgroup group in nuclide mean std. dev. - x y z -0 1 1 1 1 1 total 0.000227 0.000023 -1 1 1 1 2 1 total 0.001210 0.000119 -2 1 1 1 3 1 total 0.001176 0.000114 -3 1 1 1 4 1 total 0.002722 0.000261 -4 1 1 1 5 1 total 0.001204 0.000112 -5 1 1 1 6 1 total 0.000501 0.000047 -12 1 2 1 1 1 total 0.000227 0.000017 -13 1 2 1 2 1 total 0.001208 0.000087 -14 1 2 1 3 1 total 0.001174 0.000084 -15 1 2 1 4 1 total 0.002710 0.000192 -16 1 2 1 5 1 total 0.001194 0.000082 -17 1 2 1 6 1 total 0.000497 0.000034 -6 2 1 1 1 1 total 0.000227 0.000010 -7 2 1 1 2 1 total 0.001210 0.000053 -8 2 1 1 3 1 total 0.001177 0.000052 -9 2 1 1 4 1 total 0.002726 0.000119 -10 2 1 1 5 1 total 0.001208 0.000053 -11 2 1 1 6 1 total 0.000503 0.000022 -18 2 2 1 1 1 total 0.000227 0.000019 -19 2 2 1 2 1 total 0.001211 0.000102 -20 2 2 1 3 1 total 0.001178 0.000098 -21 2 2 1 4 1 total 0.002726 0.000223 -22 2 2 1 5 1 total 0.001207 0.000096 -23 2 2 1 6 1 total 0.000503 0.000040 - mesh 1 delayedgroup nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.013353 0.001345 -1 1 1 1 2 total 0.032619 0.003120 -2 1 1 1 3 total 0.121042 0.011142 -3 1 1 1 4 total 0.305503 0.026418 -4 1 1 1 5 total 0.860433 0.064665 -5 1 1 1 6 total 2.889963 0.219527 -12 1 2 1 1 total 0.013352 0.001049 -13 1 2 1 2 total 0.032626 0.002465 -14 1 2 1 3 total 0.121027 0.008891 -15 1 2 1 4 total 0.305351 0.021426 -16 1 2 1 5 total 0.859866 0.054490 -17 1 2 1 6 total 2.888034 0.184436 -6 2 1 1 1 total 0.013353 0.000612 -7 2 1 1 2 total 0.032616 0.001412 -8 2 1 1 3 total 0.121047 0.005039 -9 2 1 1 4 total 0.305559 0.012002 -10 2 1 1 5 total 0.860640 0.030707 -11 2 1 1 6 total 2.890664 0.103692 -18 2 2 1 1 total 0.013353 0.001132 -19 2 2 1 2 total 0.032617 0.002633 -20 2 2 1 3 total 0.121046 0.009426 -21 2 2 1 4 total 0.305545 0.022417 -22 2 2 1 5 total 0.860588 0.055030 -23 2 2 1 6 total 2.890489 0.186821 - mesh 1 delayedgroup group in group out nuclide mean std. dev. - x y z -0 1 1 1 1 1 1 total 0.000000 0.000000 -1 1 1 1 2 1 1 total 0.000000 0.000000 -2 1 1 1 3 1 1 total 0.000000 0.000000 -3 1 1 1 4 1 1 total 0.000219 0.000219 -4 1 1 1 5 1 1 total 0.000000 0.000000 -5 1 1 1 6 1 1 total 0.000000 0.000000 -12 1 2 1 1 1 1 total 0.000000 0.000000 -13 1 2 1 2 1 1 total 0.000000 0.000000 -14 1 2 1 3 1 1 total 0.000000 0.000000 -15 1 2 1 4 1 1 total 0.000467 0.000287 -16 1 2 1 5 1 1 total 0.000000 0.000000 -17 1 2 1 6 1 1 total 0.000000 0.000000 -6 2 1 1 1 1 1 total 0.000000 0.000000 -7 2 1 1 2 1 1 total 0.000000 0.000000 -8 2 1 1 3 1 1 total 0.000000 0.000000 -9 2 1 1 4 1 1 total 0.000211 0.000211 -10 2 1 1 5 1 1 total 0.000000 0.000000 -11 2 1 1 6 1 1 total 0.000000 0.000000 -18 2 2 1 1 1 1 total 0.000000 0.000000 -19 2 2 1 2 1 1 total 0.000000 0.000000 -20 2 2 1 3 1 1 total 0.000000 0.000000 -21 2 2 1 4 1 1 total 0.000219 0.000219 -22 2 2 1 5 1 1 total 0.000000 0.000000 -23 2 2 1 6 1 1 total 0.000000 0.000000 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.943198 0.067915 +2 1 2 1 total 0.895741 0.058714 +1 2 1 1 total 0.911011 0.068461 +3 2 2 1 total 0.927611 0.066426 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.943198 0.067915 +2 1 2 1 total 0.895741 0.058714 +1 2 1 1 total 0.911011 0.068461 +3 2 2 1 total 0.927611 0.066426 + mesh 1 delayedgroup group in nuclide mean std. dev. + x y +0 1 1 1 1 total 0.000006 4.453421e-07 +1 1 1 2 1 total 0.000032 2.303082e-06 +2 1 1 3 1 total 0.000031 2.202789e-06 +3 1 1 4 1 total 0.000072 4.961025e-06 +4 1 1 5 1 total 0.000032 2.071919e-06 +5 1 1 6 1 total 0.000013 8.662934e-07 +12 1 2 1 1 total 0.000006 3.282329e-07 +13 1 2 2 1 total 0.000031 1.701766e-06 +14 1 2 3 1 total 0.000030 1.629615e-06 +15 1 2 4 1 total 0.000070 3.675759e-06 +16 1 2 5 1 total 0.000031 1.536226e-06 +17 1 2 6 1 total 0.000013 6.423838e-07 +6 2 1 1 1 total 0.000006 2.308186e-07 +7 2 1 2 1 total 0.000032 1.211112e-06 +8 2 1 3 1 total 0.000031 1.169911e-06 +9 2 1 4 1 total 0.000072 2.685832e-06 +10 2 1 5 1 total 0.000032 1.187072e-06 +11 2 1 6 1 total 0.000013 4.939053e-07 +18 2 2 1 1 total 0.000006 3.819684e-07 +19 2 2 2 1 total 0.000032 1.976073e-06 +20 2 2 3 1 total 0.000031 1.889748e-06 +21 2 2 4 1 total 0.000072 4.252207e-06 +22 2 2 5 1 total 0.000032 1.765565e-06 +23 2 2 6 1 total 0.000013 7.386890e-07 + mesh 1 delayedgroup group out nuclide mean std. dev. + x y +0 1 1 1 1 total 0.0 0.000000 +1 1 1 2 1 total 0.0 0.000000 +2 1 1 3 1 total 0.0 0.000000 +3 1 1 4 1 total 1.0 1.414214 +4 1 1 5 1 total 0.0 0.000000 +5 1 1 6 1 total 0.0 0.000000 +12 1 2 1 1 total 0.0 0.000000 +13 1 2 2 1 total 0.0 0.000000 +14 1 2 3 1 total 0.0 0.000000 +15 1 2 4 1 total 1.0 0.869026 +16 1 2 5 1 total 0.0 0.000000 +17 1 2 6 1 total 0.0 0.000000 +6 2 1 1 1 total 0.0 0.000000 +7 2 1 2 1 total 0.0 0.000000 +8 2 1 3 1 total 0.0 0.000000 +9 2 1 4 1 total 1.0 1.414214 +10 2 1 5 1 total 0.0 0.000000 +11 2 1 6 1 total 0.0 0.000000 +18 2 2 1 1 total 0.0 0.000000 +19 2 2 2 1 total 0.0 0.000000 +20 2 2 3 1 total 0.0 0.000000 +21 2 2 4 1 total 1.0 1.414214 +22 2 2 5 1 total 0.0 0.000000 +23 2 2 6 1 total 0.0 0.000000 + mesh 1 delayedgroup group in nuclide mean std. dev. + x y +0 1 1 1 1 total 0.000227 0.000023 +1 1 1 2 1 total 0.001210 0.000119 +2 1 1 3 1 total 0.001176 0.000114 +3 1 1 4 1 total 0.002722 0.000261 +4 1 1 5 1 total 0.001204 0.000112 +5 1 1 6 1 total 0.000501 0.000047 +12 1 2 1 1 total 0.000227 0.000017 +13 1 2 2 1 total 0.001208 0.000087 +14 1 2 3 1 total 0.001174 0.000084 +15 1 2 4 1 total 0.002710 0.000192 +16 1 2 5 1 total 0.001194 0.000082 +17 1 2 6 1 total 0.000497 0.000034 +6 2 1 1 1 total 0.000227 0.000010 +7 2 1 2 1 total 0.001210 0.000053 +8 2 1 3 1 total 0.001177 0.000052 +9 2 1 4 1 total 0.002726 0.000119 +10 2 1 5 1 total 0.001208 0.000053 +11 2 1 6 1 total 0.000503 0.000022 +18 2 2 1 1 total 0.000227 0.000019 +19 2 2 2 1 total 0.001211 0.000102 +20 2 2 3 1 total 0.001178 0.000098 +21 2 2 4 1 total 0.002726 0.000223 +22 2 2 5 1 total 0.001207 0.000096 +23 2 2 6 1 total 0.000503 0.000040 + mesh 1 delayedgroup nuclide mean std. dev. + x y +0 1 1 1 total 0.013353 0.001345 +1 1 1 2 total 0.032619 0.003120 +2 1 1 3 total 0.121042 0.011142 +3 1 1 4 total 0.305503 0.026418 +4 1 1 5 total 0.860433 0.064665 +5 1 1 6 total 2.889963 0.219527 +12 1 2 1 total 0.013352 0.001049 +13 1 2 2 total 0.032626 0.002465 +14 1 2 3 total 0.121027 0.008891 +15 1 2 4 total 0.305351 0.021426 +16 1 2 5 total 0.859866 0.054490 +17 1 2 6 total 2.888034 0.184436 +6 2 1 1 total 0.013353 0.000612 +7 2 1 2 total 0.032616 0.001412 +8 2 1 3 total 0.121047 0.005039 +9 2 1 4 total 0.305559 0.012002 +10 2 1 5 total 0.860640 0.030707 +11 2 1 6 total 2.890664 0.103692 +18 2 2 1 total 0.013353 0.001132 +19 2 2 2 total 0.032617 0.002633 +20 2 2 3 total 0.121046 0.009426 +21 2 2 4 total 0.305545 0.022417 +22 2 2 5 total 0.860588 0.055030 +23 2 2 6 total 2.890489 0.186821 + mesh 1 delayedgroup group in group out nuclide mean std. dev. + x y +0 1 1 1 1 1 total 0.000000 0.000000 +1 1 1 2 1 1 total 0.000000 0.000000 +2 1 1 3 1 1 total 0.000000 0.000000 +3 1 1 4 1 1 total 0.000219 0.000219 +4 1 1 5 1 1 total 0.000000 0.000000 +5 1 1 6 1 1 total 0.000000 0.000000 +12 1 2 1 1 1 total 0.000000 0.000000 +13 1 2 2 1 1 total 0.000000 0.000000 +14 1 2 3 1 1 total 0.000000 0.000000 +15 1 2 4 1 1 total 0.000467 0.000287 +16 1 2 5 1 1 total 0.000000 0.000000 +17 1 2 6 1 1 total 0.000000 0.000000 +6 2 1 1 1 1 total 0.000000 0.000000 +7 2 1 2 1 1 total 0.000000 0.000000 +8 2 1 3 1 1 total 0.000000 0.000000 +9 2 1 4 1 1 total 0.000211 0.000211 +10 2 1 5 1 1 total 0.000000 0.000000 +11 2 1 6 1 1 total 0.000000 0.000000 +18 2 2 1 1 1 total 0.000000 0.000000 +19 2 2 2 1 1 total 0.000000 0.000000 +20 2 2 3 1 1 total 0.000000 0.000000 +21 2 2 4 1 1 total 0.000219 0.000219 +22 2 2 5 1 1 total 0.000000 0.000000 +23 2 2 6 1 1 total 0.000000 0.000000 diff --git a/tests/regression_tests/mgxs_library_mesh/results_true.dat b/tests/regression_tests/mgxs_library_mesh/results_true.dat index 16b86870d7..ec7620c5c5 100644 --- a/tests/regression_tests/mgxs_library_mesh/results_true.dat +++ b/tests/regression_tests/mgxs_library_mesh/results_true.dat @@ -1,189 +1,189 @@ - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.103374 0.004981 -2 1 2 1 1 total 0.103852 0.004752 -1 2 1 1 1 total 0.103322 0.003613 -3 2 2 1 1 total 0.102889 0.003387 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.072760 0.005235 -2 1 2 1 1 total 0.074856 0.004972 -1 2 1 1 1 total 0.074583 0.004000 -3 2 2 1 1 total 0.072925 0.003485 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.072768 0.005236 -2 1 2 1 1 total 0.074864 0.004972 -1 2 1 1 1 total 0.074603 0.004002 -3 2 2 1 1 total 0.072881 0.003491 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.013309 0.000729 -2 1 2 1 1 total 0.013223 0.000707 -1 2 1 1 1 total 0.013222 0.000596 -3 2 2 1 1 total 0.013282 0.000564 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.013241 0.000728 -2 1 2 1 1 total 0.013142 0.000705 -1 2 1 1 1 total 0.013137 0.000596 -3 2 2 1 1 total 0.013221 0.000564 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.001281 0.000874 -2 1 2 1 1 total 0.001297 0.000731 -1 2 1 1 1 total 0.001281 0.000744 -3 2 2 1 1 total 0.001288 0.000719 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.012029 0.000664 -2 1 2 1 1 total 0.011926 0.000639 -1 2 1 1 1 total 0.011941 0.000545 -3 2 2 1 1 total 0.011994 0.000515 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.031361 0.001735 -2 1 2 1 1 total 0.031091 0.001675 -1 2 1 1 1 total 0.031169 0.001418 -3 2 2 1 1 total 0.031255 0.001366 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 2.326447e+06 128504.886535 -2 1 2 1 1 total 2.306518e+06 123638.096130 -1 2 1 1 1 total 2.309435e+06 105395.059055 -3 2 2 1 1 total 2.319667e+06 99692.620001 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.090065 0.004262 -2 1 2 1 1 total 0.090629 0.004086 -1 2 1 1 1 total 0.090100 0.003045 -3 2 2 1 1 total 0.089607 0.002828 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.090188 0.005151 -2 1 2 1 1 total 0.094339 0.004349 -1 2 1 1 1 total 0.088294 0.003953 -3 2 2 1 1 total 0.089633 0.002592 - mesh 1 group in group out legendre nuclide mean std. dev. - x y z -0 1 1 1 1 1 P0 total 0.090122 0.005149 -1 1 1 1 1 1 P1 total 0.030614 0.001612 -2 1 1 1 1 1 P2 total 0.016490 0.000897 -3 1 1 1 1 1 P3 total 0.010163 0.000829 -8 1 2 1 1 1 P0 total 0.094307 0.004337 -9 1 2 1 1 1 P1 total 0.028996 0.001462 -10 1 2 1 1 1 P2 total 0.017486 0.000808 -11 1 2 1 1 1 P3 total 0.009753 0.000742 -4 2 1 1 1 1 P0 total 0.088198 0.003959 -5 2 1 1 1 1 P1 total 0.028739 0.001715 -6 2 1 1 1 1 P2 total 0.015608 0.000680 -7 2 1 1 1 1 P3 total 0.008232 0.000363 -12 2 2 1 1 1 P0 total 0.089536 0.002551 -13 2 2 1 1 1 P1 total 0.029964 0.000821 -14 2 2 1 1 1 P2 total 0.015742 0.001260 -15 2 2 1 1 1 P3 total 0.008944 0.000385 - mesh 1 group in group out legendre nuclide mean std. dev. - x y z -0 1 1 1 1 1 P0 total 0.090188 0.005151 -1 1 1 1 1 1 P1 total 0.030606 0.001614 -2 1 1 1 1 1 P2 total 0.016462 0.000898 -3 1 1 1 1 1 P3 total 0.010173 0.000824 -8 1 2 1 1 1 P0 total 0.094339 0.004349 -9 1 2 1 1 1 P1 total 0.028988 0.001462 -10 1 2 1 1 1 P2 total 0.017473 0.000811 -11 1 2 1 1 1 P3 total 0.009763 0.000749 -4 2 1 1 1 1 P0 total 0.088294 0.003953 -5 2 1 1 1 1 P1 total 0.028719 0.001720 -6 2 1 1 1 1 P2 total 0.015571 0.000691 -7 2 1 1 1 1 P3 total 0.008255 0.000361 -12 2 2 1 1 1 P0 total 0.089633 0.002592 -13 2 2 1 1 1 P1 total 0.030008 0.000847 -14 2 2 1 1 1 P2 total 0.015748 0.001284 -15 2 2 1 1 1 P3 total 0.008951 0.000387 - mesh 1 group in group out nuclide mean std. dev. - x y z -0 1 1 1 1 1 total 1.000736 0.060260 -2 1 2 1 1 1 total 1.000346 0.042656 -1 2 1 1 1 1 total 1.001091 0.052736 -3 2 2 1 1 1 total 1.001078 0.038212 - mesh 1 group in group out nuclide mean std. dev. - x y z -0 1 1 1 1 1 total 0.031890 0.002523 -2 1 2 1 1 1 total 0.032714 0.001502 -1 2 1 1 1 1 total 0.030414 0.002369 -3 2 2 1 1 1 total 0.031051 0.001521 - mesh 1 group in group out nuclide mean std. dev. - x y z -0 1 1 1 1 1 total 1.0 0.060234 -2 1 2 1 1 1 total 1.0 0.042523 -1 2 1 1 1 1 total 1.0 0.052777 -3 2 2 1 1 1 total 1.0 0.037842 - mesh 1 group in group out legendre nuclide mean std. dev. - x y z -0 1 1 1 1 1 P0 total 0.090065 0.006899 -1 1 1 1 1 1 P1 total 0.030595 0.002243 -2 1 1 1 1 1 P2 total 0.016480 0.001229 -3 1 1 1 1 1 P3 total 0.010157 0.000977 -8 1 2 1 1 1 P0 total 0.090629 0.005616 -9 1 2 1 1 1 P1 total 0.027865 0.001820 -10 1 2 1 1 1 P2 total 0.016804 0.001044 -11 1 2 1 1 1 P3 total 0.009373 0.000813 -4 2 1 1 1 1 P0 total 0.090100 0.005647 -5 2 1 1 1 1 P1 total 0.029359 0.002172 -6 2 1 1 1 1 P2 total 0.015944 0.000984 -7 2 1 1 1 1 P3 total 0.008410 0.000523 -12 2 2 1 1 1 P0 total 0.089607 0.004415 -13 2 2 1 1 1 P1 total 0.029988 0.001459 -14 2 2 1 1 1 P2 total 0.015755 0.001411 -15 2 2 1 1 1 P3 total 0.008951 0.000528 - mesh 1 group in group out legendre nuclide mean std. dev. - x y z -0 1 1 1 1 1 P0 total 0.090131 0.008782 -1 1 1 1 1 1 P1 total 0.030617 0.002905 -2 1 1 1 1 1 P2 total 0.016492 0.001581 -3 1 1 1 1 1 P3 total 0.010164 0.001154 -8 1 2 1 1 1 P0 total 0.090661 0.006820 -9 1 2 1 1 1 P1 total 0.027875 0.002174 -10 1 2 1 1 1 P2 total 0.016810 0.001267 -11 1 2 1 1 1 P3 total 0.009376 0.000906 -4 2 1 1 1 1 P0 total 0.090199 0.007385 -5 2 1 1 1 1 P1 total 0.029391 0.002669 -6 2 1 1 1 1 P2 total 0.015962 0.001295 -7 2 1 1 1 1 P3 total 0.008419 0.000686 -12 2 2 1 1 1 P0 total 0.089704 0.005591 -13 2 2 1 1 1 P1 total 0.030020 0.001856 -14 2 2 1 1 1 P2 total 0.015772 0.001536 -15 2 2 1 1 1 P3 total 0.008961 0.000629 - mesh 1 group out nuclide mean std. dev. - x y z -0 1 1 1 1 total 1.0 0.098093 -2 1 2 1 1 total 1.0 0.042346 -1 2 1 1 1 total 1.0 0.104352 -3 2 2 1 1 total 1.0 0.067889 - mesh 1 group out nuclide mean std. dev. - x y z -0 1 1 1 1 total 1.0 0.099401 -2 1 2 1 1 total 1.0 0.042085 -1 2 1 1 1 total 1.0 0.104135 -3 2 2 1 1 total 1.0 0.067307 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 8.547713e-10 3.477664e-11 -2 1 2 1 1 total 9.057083e-10 4.171858e-11 -1 2 1 1 1 total 8.666731e-10 2.371072e-11 -3 2 2 1 1 total 8.532016e-10 1.638929e-11 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.031163 0.001724 -2 1 2 1 1 total 0.030895 0.001664 -1 2 1 1 1 total 0.030973 0.001409 -3 2 2 1 1 total 0.031058 0.001358 - mesh 1 group in group out nuclide mean std. dev. - x y z -0 1 1 1 1 1 total 0.031781 0.002541 -2 1 2 1 1 1 total 0.032490 0.001487 -1 2 1 1 1 1 total 0.030274 0.002354 -3 2 2 1 1 1 total 0.030882 0.001500 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.103374 0.004981 +2 1 2 1 total 0.103852 0.004752 +1 2 1 1 total 0.103322 0.003613 +3 2 2 1 total 0.102889 0.003387 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.072760 0.005235 +2 1 2 1 total 0.074856 0.004972 +1 2 1 1 total 0.074583 0.004000 +3 2 2 1 total 0.072925 0.003485 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.072768 0.005236 +2 1 2 1 total 0.074864 0.004972 +1 2 1 1 total 0.074603 0.004002 +3 2 2 1 total 0.072881 0.003491 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.013309 0.000729 +2 1 2 1 total 0.013223 0.000707 +1 2 1 1 total 0.013222 0.000596 +3 2 2 1 total 0.013282 0.000564 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.013241 0.000728 +2 1 2 1 total 0.013142 0.000705 +1 2 1 1 total 0.013137 0.000596 +3 2 2 1 total 0.013221 0.000564 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.001281 0.000874 +2 1 2 1 total 0.001297 0.000731 +1 2 1 1 total 0.001281 0.000744 +3 2 2 1 total 0.001288 0.000719 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.012029 0.000664 +2 1 2 1 total 0.011926 0.000639 +1 2 1 1 total 0.011941 0.000545 +3 2 2 1 total 0.011994 0.000515 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.031361 0.001735 +2 1 2 1 total 0.031091 0.001675 +1 2 1 1 total 0.031169 0.001418 +3 2 2 1 total 0.031255 0.001366 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 2.326447e+06 128504.886535 +2 1 2 1 total 2.306518e+06 123638.096130 +1 2 1 1 total 2.309435e+06 105395.059055 +3 2 2 1 total 2.319667e+06 99692.620001 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.090065 0.004262 +2 1 2 1 total 0.090629 0.004086 +1 2 1 1 total 0.090100 0.003045 +3 2 2 1 total 0.089607 0.002828 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.090188 0.005151 +2 1 2 1 total 0.094339 0.004349 +1 2 1 1 total 0.088294 0.003953 +3 2 2 1 total 0.089633 0.002592 + mesh 1 group in group out legendre nuclide mean std. dev. + x y +0 1 1 1 1 P0 total 0.090122 0.005149 +1 1 1 1 1 P1 total 0.030614 0.001612 +2 1 1 1 1 P2 total 0.016490 0.000897 +3 1 1 1 1 P3 total 0.010163 0.000829 +8 1 2 1 1 P0 total 0.094307 0.004337 +9 1 2 1 1 P1 total 0.028996 0.001462 +10 1 2 1 1 P2 total 0.017486 0.000808 +11 1 2 1 1 P3 total 0.009753 0.000742 +4 2 1 1 1 P0 total 0.088198 0.003959 +5 2 1 1 1 P1 total 0.028739 0.001715 +6 2 1 1 1 P2 total 0.015608 0.000680 +7 2 1 1 1 P3 total 0.008232 0.000363 +12 2 2 1 1 P0 total 0.089536 0.002551 +13 2 2 1 1 P1 total 0.029964 0.000821 +14 2 2 1 1 P2 total 0.015742 0.001260 +15 2 2 1 1 P3 total 0.008944 0.000385 + mesh 1 group in group out legendre nuclide mean std. dev. + x y +0 1 1 1 1 P0 total 0.090188 0.005151 +1 1 1 1 1 P1 total 0.030606 0.001614 +2 1 1 1 1 P2 total 0.016462 0.000898 +3 1 1 1 1 P3 total 0.010173 0.000824 +8 1 2 1 1 P0 total 0.094339 0.004349 +9 1 2 1 1 P1 total 0.028988 0.001462 +10 1 2 1 1 P2 total 0.017473 0.000811 +11 1 2 1 1 P3 total 0.009763 0.000749 +4 2 1 1 1 P0 total 0.088294 0.003953 +5 2 1 1 1 P1 total 0.028719 0.001720 +6 2 1 1 1 P2 total 0.015571 0.000691 +7 2 1 1 1 P3 total 0.008255 0.000361 +12 2 2 1 1 P0 total 0.089633 0.002592 +13 2 2 1 1 P1 total 0.030008 0.000847 +14 2 2 1 1 P2 total 0.015748 0.001284 +15 2 2 1 1 P3 total 0.008951 0.000387 + mesh 1 group in group out nuclide mean std. dev. + x y +0 1 1 1 1 total 1.000736 0.060260 +2 1 2 1 1 total 1.000346 0.042656 +1 2 1 1 1 total 1.001091 0.052736 +3 2 2 1 1 total 1.001078 0.038212 + mesh 1 group in group out nuclide mean std. dev. + x y +0 1 1 1 1 total 0.031890 0.002523 +2 1 2 1 1 total 0.032714 0.001502 +1 2 1 1 1 total 0.030414 0.002369 +3 2 2 1 1 total 0.031051 0.001521 + mesh 1 group in group out nuclide mean std. dev. + x y +0 1 1 1 1 total 1.0 0.060234 +2 1 2 1 1 total 1.0 0.042523 +1 2 1 1 1 total 1.0 0.052777 +3 2 2 1 1 total 1.0 0.037842 + mesh 1 group in group out legendre nuclide mean std. dev. + x y +0 1 1 1 1 P0 total 0.090065 0.006899 +1 1 1 1 1 P1 total 0.030595 0.002243 +2 1 1 1 1 P2 total 0.016480 0.001229 +3 1 1 1 1 P3 total 0.010157 0.000977 +8 1 2 1 1 P0 total 0.090629 0.005616 +9 1 2 1 1 P1 total 0.027865 0.001820 +10 1 2 1 1 P2 total 0.016804 0.001044 +11 1 2 1 1 P3 total 0.009373 0.000813 +4 2 1 1 1 P0 total 0.090100 0.005647 +5 2 1 1 1 P1 total 0.029359 0.002172 +6 2 1 1 1 P2 total 0.015944 0.000984 +7 2 1 1 1 P3 total 0.008410 0.000523 +12 2 2 1 1 P0 total 0.089607 0.004415 +13 2 2 1 1 P1 total 0.029988 0.001459 +14 2 2 1 1 P2 total 0.015755 0.001411 +15 2 2 1 1 P3 total 0.008951 0.000528 + mesh 1 group in group out legendre nuclide mean std. dev. + x y +0 1 1 1 1 P0 total 0.090131 0.008782 +1 1 1 1 1 P1 total 0.030617 0.002905 +2 1 1 1 1 P2 total 0.016492 0.001581 +3 1 1 1 1 P3 total 0.010164 0.001154 +8 1 2 1 1 P0 total 0.090661 0.006820 +9 1 2 1 1 P1 total 0.027875 0.002174 +10 1 2 1 1 P2 total 0.016810 0.001267 +11 1 2 1 1 P3 total 0.009376 0.000906 +4 2 1 1 1 P0 total 0.090199 0.007385 +5 2 1 1 1 P1 total 0.029391 0.002669 +6 2 1 1 1 P2 total 0.015962 0.001295 +7 2 1 1 1 P3 total 0.008419 0.000686 +12 2 2 1 1 P0 total 0.089704 0.005591 +13 2 2 1 1 P1 total 0.030020 0.001856 +14 2 2 1 1 P2 total 0.015772 0.001536 +15 2 2 1 1 P3 total 0.008961 0.000629 + mesh 1 group out nuclide mean std. dev. + x y +0 1 1 1 total 1.0 0.098093 +2 1 2 1 total 1.0 0.042346 +1 2 1 1 total 1.0 0.104352 +3 2 2 1 total 1.0 0.067889 + mesh 1 group out nuclide mean std. dev. + x y +0 1 1 1 total 1.0 0.099401 +2 1 2 1 total 1.0 0.042085 +1 2 1 1 total 1.0 0.104135 +3 2 2 1 total 1.0 0.067307 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 8.547713e-10 3.477664e-11 +2 1 2 1 total 9.057083e-10 4.171858e-11 +1 2 1 1 total 8.666731e-10 2.371072e-11 +3 2 2 1 total 8.532016e-10 1.638929e-11 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 0.031163 0.001724 +2 1 2 1 total 0.030895 0.001664 +1 2 1 1 total 0.030973 0.001409 +3 2 2 1 total 0.031058 0.001358 + mesh 1 group in group out nuclide mean std. dev. + x y +0 1 1 1 1 total 0.031781 0.002541 +2 1 2 1 1 total 0.032490 0.001487 +1 2 1 1 1 total 0.030274 0.002354 +3 2 2 1 1 total 0.030882 0.001500 mesh 1 group in nuclide mean std. dev. x y surf 3 1 1 x-max in 1 total 0.1888 0.008218 @@ -218,145 +218,145 @@ 30 2 2 y-max out 1 total 0.0000 0.000000 29 2 2 y-min in 1 total 0.1870 0.009597 28 2 2 y-min out 1 total 0.1850 0.011256 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 4.581283 0.329623 -2 1 2 1 1 total 4.452968 0.295762 -1 2 1 1 1 total 4.469295 0.239674 -3 2 2 1 1 total 4.570894 0.218416 - mesh 1 group in nuclide mean std. dev. - x y z -0 1 1 1 1 total 4.580746 0.329583 -2 1 2 1 1 total 4.452519 0.295716 -1 2 1 1 1 total 4.468066 0.239680 -3 2 2 1 1 total 4.573657 0.219069 - mesh 1 delayedgroup group in nuclide mean std. dev. - x y z -0 1 1 1 1 1 total 0.000007 3.812309e-07 -1 1 1 1 2 1 total 0.000036 1.967797e-06 -2 1 1 1 3 1 total 0.000034 1.878630e-06 -3 1 1 1 4 1 total 0.000077 4.212043e-06 -4 1 1 1 5 1 total 0.000031 1.726878e-06 -5 1 1 1 6 1 total 0.000013 7.233832e-07 -12 1 2 1 1 1 total 0.000007 3.663985e-07 -13 1 2 1 2 1 total 0.000035 1.891236e-06 -14 1 2 1 3 1 total 0.000034 1.805539e-06 -15 1 2 1 4 1 total 0.000076 4.048166e-06 -16 1 2 1 5 1 total 0.000031 1.659691e-06 -17 1 2 1 6 1 total 0.000013 6.952388e-07 -6 2 1 1 1 1 total 0.000007 3.123173e-07 -7 2 1 1 2 1 total 0.000035 1.612086e-06 -8 2 1 1 3 1 total 0.000034 1.539037e-06 -9 2 1 1 4 1 total 0.000076 3.450649e-06 -10 2 1 1 5 1 total 0.000031 1.414717e-06 -11 2 1 1 6 1 total 0.000013 5.926201e-07 -18 2 2 1 1 1 total 0.000007 2.946716e-07 -19 2 2 1 2 1 total 0.000036 1.521004e-06 -20 2 2 1 3 1 total 0.000034 1.452083e-06 -21 2 2 1 4 1 total 0.000076 3.255689e-06 -22 2 2 1 5 1 total 0.000031 1.334787e-06 -23 2 2 1 6 1 total 0.000013 5.591374e-07 - mesh 1 delayedgroup group out nuclide mean std. dev. - x y z -0 1 1 1 1 1 total 1.0 1.414214 -1 1 1 1 2 1 total 0.0 0.000000 -2 1 1 1 3 1 total 0.0 0.000000 -3 1 1 1 4 1 total 1.0 0.578922 -4 1 1 1 5 1 total 0.0 0.000000 -5 1 1 1 6 1 total 0.0 0.000000 -12 1 2 1 1 1 total 0.0 0.000000 -13 1 2 1 2 1 total 1.0 0.578922 -14 1 2 1 3 1 total 1.0 1.414214 -15 1 2 1 4 1 total 1.0 1.414214 -16 1 2 1 5 1 total 1.0 0.875472 -17 1 2 1 6 1 total 1.0 1.414214 -6 2 1 1 1 1 total 0.0 0.000000 -7 2 1 1 2 1 total 0.0 0.000000 -8 2 1 1 3 1 total 1.0 1.414214 -9 2 1 1 4 1 total 1.0 0.579392 -10 2 1 1 5 1 total 1.0 1.414214 -11 2 1 1 6 1 total 0.0 0.000000 -18 2 2 1 1 1 total 1.0 0.868163 -19 2 2 1 2 1 total 1.0 1.414214 -20 2 2 1 3 1 total 0.0 0.000000 -21 2 2 1 4 1 total 1.0 0.868969 -22 2 2 1 5 1 total 1.0 1.414214 -23 2 2 1 6 1 total 0.0 0.000000 - mesh 1 delayedgroup group in nuclide mean std. dev. - x y z -0 1 1 1 1 1 total 0.000221 0.000015 -1 1 1 1 2 1 total 0.001140 0.000079 -2 1 1 1 3 1 total 0.001088 0.000075 -3 1 1 1 4 1 total 0.002440 0.000169 -4 1 1 1 5 1 total 0.001001 0.000069 -5 1 1 1 6 1 total 0.000419 0.000029 -12 1 2 1 1 1 total 0.000221 0.000013 -13 1 2 1 2 1 total 0.001139 0.000066 -14 1 2 1 3 1 total 0.001088 0.000063 -15 1 2 1 4 1 total 0.002439 0.000142 -16 1 2 1 5 1 total 0.001000 0.000058 -17 1 2 1 6 1 total 0.000419 0.000024 -6 2 1 1 1 1 total 0.000220 0.000013 -7 2 1 1 2 1 total 0.001138 0.000067 -8 2 1 1 3 1 total 0.001086 0.000064 -9 2 1 1 4 1 total 0.002435 0.000144 -10 2 1 1 5 1 total 0.000998 0.000059 -11 2 1 1 6 1 total 0.000418 0.000025 -18 2 2 1 1 1 total 0.000221 0.000013 -19 2 2 1 2 1 total 0.001141 0.000066 -20 2 2 1 3 1 total 0.001090 0.000063 -21 2 2 1 4 1 total 0.002443 0.000141 -22 2 2 1 5 1 total 0.001002 0.000058 -23 2 2 1 6 1 total 0.000420 0.000024 - mesh 1 delayedgroup nuclide mean std. dev. - x y z -0 1 1 1 1 total 0.013336 0.000919 -1 1 1 1 2 total 0.032739 0.002257 -2 1 1 1 3 total 0.120780 0.008325 -3 1 1 1 4 total 0.302780 0.020871 -4 1 1 1 5 total 0.849490 0.058555 -5 1 1 1 6 total 2.853000 0.196656 -12 1 2 1 1 total 0.013336 0.000770 -13 1 2 1 2 total 0.032739 0.001891 -14 1 2 1 3 total 0.120780 0.006977 -15 1 2 1 4 total 0.302780 0.017490 -16 1 2 1 5 total 0.849490 0.049071 -17 1 2 1 6 total 2.853000 0.164804 -6 2 1 1 1 total 0.013336 0.000790 -7 2 1 1 2 total 0.032739 0.001940 -8 2 1 1 3 total 0.120780 0.007157 -9 2 1 1 4 total 0.302780 0.017942 -10 2 1 1 5 total 0.849490 0.050338 -11 2 1 1 6 total 2.853000 0.169061 -18 2 2 1 1 total 0.013336 0.000757 -19 2 2 1 2 total 0.032739 0.001858 -20 2 2 1 3 total 0.120780 0.006855 -21 2 2 1 4 total 0.302780 0.017186 -22 2 2 1 5 total 0.849490 0.048217 -23 2 2 1 6 total 2.853000 0.161935 - mesh 1 delayedgroup group in group out nuclide mean std. dev. - x y z -0 1 1 1 1 1 1 total 0.000026 0.000026 -1 1 1 1 2 1 1 total 0.000000 0.000000 -2 1 1 1 3 1 1 total 0.000000 0.000000 -3 1 1 1 4 1 1 total 0.000083 0.000034 -4 1 1 1 5 1 1 total 0.000000 0.000000 -5 1 1 1 6 1 1 total 0.000000 0.000000 -12 1 2 1 1 1 1 total 0.000000 0.000000 -13 1 2 1 2 1 1 total 0.000081 0.000033 -14 1 2 1 3 1 1 total 0.000026 0.000026 -15 1 2 1 4 1 1 total 0.000026 0.000026 -16 1 2 1 5 1 1 total 0.000059 0.000036 -17 1 2 1 6 1 1 total 0.000033 0.000033 -6 2 1 1 1 1 1 total 0.000000 0.000000 -7 2 1 1 2 1 1 total 0.000000 0.000000 -8 2 1 1 3 1 1 total 0.000032 0.000032 -9 2 1 1 4 1 1 total 0.000080 0.000033 -10 2 1 1 5 1 1 total 0.000028 0.000028 -11 2 1 1 6 1 1 total 0.000000 0.000000 -18 2 2 1 1 1 1 total 0.000054 0.000033 -19 2 2 1 2 1 1 total 0.000029 0.000029 -20 2 2 1 3 1 1 total 0.000000 0.000000 -21 2 2 1 4 1 1 total 0.000054 0.000033 -22 2 2 1 5 1 1 total 0.000032 0.000032 -23 2 2 1 6 1 1 total 0.000000 0.000000 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 4.581283 0.329623 +2 1 2 1 total 4.452968 0.295762 +1 2 1 1 total 4.469295 0.239674 +3 2 2 1 total 4.570894 0.218416 + mesh 1 group in nuclide mean std. dev. + x y +0 1 1 1 total 4.580746 0.329583 +2 1 2 1 total 4.452519 0.295716 +1 2 1 1 total 4.468066 0.239680 +3 2 2 1 total 4.573657 0.219069 + mesh 1 delayedgroup group in nuclide mean std. dev. + x y +0 1 1 1 1 total 0.000007 3.812309e-07 +1 1 1 2 1 total 0.000036 1.967797e-06 +2 1 1 3 1 total 0.000034 1.878630e-06 +3 1 1 4 1 total 0.000077 4.212043e-06 +4 1 1 5 1 total 0.000031 1.726878e-06 +5 1 1 6 1 total 0.000013 7.233832e-07 +12 1 2 1 1 total 0.000007 3.663985e-07 +13 1 2 2 1 total 0.000035 1.891236e-06 +14 1 2 3 1 total 0.000034 1.805539e-06 +15 1 2 4 1 total 0.000076 4.048166e-06 +16 1 2 5 1 total 0.000031 1.659691e-06 +17 1 2 6 1 total 0.000013 6.952388e-07 +6 2 1 1 1 total 0.000007 3.123173e-07 +7 2 1 2 1 total 0.000035 1.612086e-06 +8 2 1 3 1 total 0.000034 1.539037e-06 +9 2 1 4 1 total 0.000076 3.450649e-06 +10 2 1 5 1 total 0.000031 1.414717e-06 +11 2 1 6 1 total 0.000013 5.926201e-07 +18 2 2 1 1 total 0.000007 2.946716e-07 +19 2 2 2 1 total 0.000036 1.521004e-06 +20 2 2 3 1 total 0.000034 1.452083e-06 +21 2 2 4 1 total 0.000076 3.255689e-06 +22 2 2 5 1 total 0.000031 1.334787e-06 +23 2 2 6 1 total 0.000013 5.591374e-07 + mesh 1 delayedgroup group out nuclide mean std. dev. + x y +0 1 1 1 1 total 1.0 1.414214 +1 1 1 2 1 total 0.0 0.000000 +2 1 1 3 1 total 0.0 0.000000 +3 1 1 4 1 total 1.0 0.578922 +4 1 1 5 1 total 0.0 0.000000 +5 1 1 6 1 total 0.0 0.000000 +12 1 2 1 1 total 0.0 0.000000 +13 1 2 2 1 total 1.0 0.578922 +14 1 2 3 1 total 1.0 1.414214 +15 1 2 4 1 total 1.0 1.414214 +16 1 2 5 1 total 1.0 0.875472 +17 1 2 6 1 total 1.0 1.414214 +6 2 1 1 1 total 0.0 0.000000 +7 2 1 2 1 total 0.0 0.000000 +8 2 1 3 1 total 1.0 1.414214 +9 2 1 4 1 total 1.0 0.579392 +10 2 1 5 1 total 1.0 1.414214 +11 2 1 6 1 total 0.0 0.000000 +18 2 2 1 1 total 1.0 0.868163 +19 2 2 2 1 total 1.0 1.414214 +20 2 2 3 1 total 0.0 0.000000 +21 2 2 4 1 total 1.0 0.868969 +22 2 2 5 1 total 1.0 1.414214 +23 2 2 6 1 total 0.0 0.000000 + mesh 1 delayedgroup group in nuclide mean std. dev. + x y +0 1 1 1 1 total 0.000221 0.000015 +1 1 1 2 1 total 0.001140 0.000079 +2 1 1 3 1 total 0.001088 0.000075 +3 1 1 4 1 total 0.002440 0.000169 +4 1 1 5 1 total 0.001001 0.000069 +5 1 1 6 1 total 0.000419 0.000029 +12 1 2 1 1 total 0.000221 0.000013 +13 1 2 2 1 total 0.001139 0.000066 +14 1 2 3 1 total 0.001088 0.000063 +15 1 2 4 1 total 0.002439 0.000142 +16 1 2 5 1 total 0.001000 0.000058 +17 1 2 6 1 total 0.000419 0.000024 +6 2 1 1 1 total 0.000220 0.000013 +7 2 1 2 1 total 0.001138 0.000067 +8 2 1 3 1 total 0.001086 0.000064 +9 2 1 4 1 total 0.002435 0.000144 +10 2 1 5 1 total 0.000998 0.000059 +11 2 1 6 1 total 0.000418 0.000025 +18 2 2 1 1 total 0.000221 0.000013 +19 2 2 2 1 total 0.001141 0.000066 +20 2 2 3 1 total 0.001090 0.000063 +21 2 2 4 1 total 0.002443 0.000141 +22 2 2 5 1 total 0.001002 0.000058 +23 2 2 6 1 total 0.000420 0.000024 + mesh 1 delayedgroup nuclide mean std. dev. + x y +0 1 1 1 total 0.013336 0.000919 +1 1 1 2 total 0.032739 0.002257 +2 1 1 3 total 0.120780 0.008325 +3 1 1 4 total 0.302780 0.020871 +4 1 1 5 total 0.849490 0.058555 +5 1 1 6 total 2.853000 0.196656 +12 1 2 1 total 0.013336 0.000770 +13 1 2 2 total 0.032739 0.001891 +14 1 2 3 total 0.120780 0.006977 +15 1 2 4 total 0.302780 0.017490 +16 1 2 5 total 0.849490 0.049071 +17 1 2 6 total 2.853000 0.164804 +6 2 1 1 total 0.013336 0.000790 +7 2 1 2 total 0.032739 0.001940 +8 2 1 3 total 0.120780 0.007157 +9 2 1 4 total 0.302780 0.017942 +10 2 1 5 total 0.849490 0.050338 +11 2 1 6 total 2.853000 0.169061 +18 2 2 1 total 0.013336 0.000757 +19 2 2 2 total 0.032739 0.001858 +20 2 2 3 total 0.120780 0.006855 +21 2 2 4 total 0.302780 0.017186 +22 2 2 5 total 0.849490 0.048217 +23 2 2 6 total 2.853000 0.161935 + mesh 1 delayedgroup group in group out nuclide mean std. dev. + x y +0 1 1 1 1 1 total 0.000026 0.000026 +1 1 1 2 1 1 total 0.000000 0.000000 +2 1 1 3 1 1 total 0.000000 0.000000 +3 1 1 4 1 1 total 0.000083 0.000034 +4 1 1 5 1 1 total 0.000000 0.000000 +5 1 1 6 1 1 total 0.000000 0.000000 +12 1 2 1 1 1 total 0.000000 0.000000 +13 1 2 2 1 1 total 0.000081 0.000033 +14 1 2 3 1 1 total 0.000026 0.000026 +15 1 2 4 1 1 total 0.000026 0.000026 +16 1 2 5 1 1 total 0.000059 0.000036 +17 1 2 6 1 1 total 0.000033 0.000033 +6 2 1 1 1 1 total 0.000000 0.000000 +7 2 1 2 1 1 total 0.000000 0.000000 +8 2 1 3 1 1 total 0.000032 0.000032 +9 2 1 4 1 1 total 0.000080 0.000033 +10 2 1 5 1 1 total 0.000028 0.000028 +11 2 1 6 1 1 total 0.000000 0.000000 +18 2 2 1 1 1 total 0.000054 0.000033 +19 2 2 2 1 1 total 0.000029 0.000029 +20 2 2 3 1 1 total 0.000000 0.000000 +21 2 2 4 1 1 total 0.000054 0.000033 +22 2 2 5 1 1 total 0.000032 0.000032 +23 2 2 6 1 1 total 0.000000 0.000000 diff --git a/tests/unit_tests/dagmc/test_lost_particles.py b/tests/unit_tests/dagmc/test_lost_particles.py index 3a4166009d..931a37f64f 100644 --- a/tests/unit_tests/dagmc/test_lost_particles.py +++ b/tests/unit_tests/dagmc/test_lost_particles.py @@ -57,6 +57,10 @@ def broken_dagmc_model(request): model.settings.inactive = 2 model.settings.output = {'summary': False} + # Limit max particle events to prevent particles from getting stuck in the + # implicit complement of the non-root DAGMC universe. + model.settings.max_particle_events = 100 + model.export_to_xml() return model diff --git a/tests/unit_tests/test_filter_mesh.py b/tests/unit_tests/test_filter_mesh.py index faa43af474..e26bea337b 100644 --- a/tests/unit_tests/test_filter_mesh.py +++ b/tests/unit_tests/test_filter_mesh.py @@ -285,3 +285,128 @@ def test_mesh_filter_rotation_roundtrip(run_in_tmpdir): elem = mesh_filter.to_xml_element() mesh_filter_xml = openmc.MeshFilter.from_xml_element(elem, meshes={mesh.id: mesh}) assert np.allclose(mesh_filter_xml.rotation, mesh_filter.rotation) + + +def test_mesh_filter_dataframe_regular_3d(): + """Test MeshFilter.get_pandas_dataframe with a 3D RegularMesh.""" + mesh = openmc.RegularMesh() + mesh.lower_left = [0, 0, 0] + mesh.upper_right = [3, 4, 5] + mesh.dimension = [3, 4, 5] + + f = openmc.MeshFilter(mesh) + data_size = 3 * 4 * 5 + df = f.get_pandas_dataframe(data_size, stride=1) + + mesh_key = f'mesh {mesh.id}' + assert (mesh_key, 'x') in df.columns + assert (mesh_key, 'y') in df.columns + assert (mesh_key, 'z') in df.columns + assert len(df) == data_size + # x varies fastest (stride=1), then y (stride=3), then z (stride=12) + assert list(df[(mesh_key, 'x')][:3]) == [1, 2, 3] + assert df[(mesh_key, 'y')].iloc[0] == 1 + assert df[(mesh_key, 'y')].iloc[3] == 2 + + +def test_mesh_filter_dataframe_regular_2d(): + """Test MeshFilter.get_pandas_dataframe with a 2D RegularMesh.""" + mesh = openmc.RegularMesh() + mesh.lower_left = [0, 0] + mesh.upper_right = [2, 3] + mesh.dimension = [2, 3] + + f = openmc.MeshFilter(mesh) + data_size = 2 * 3 + df = f.get_pandas_dataframe(data_size, stride=1) + + mesh_key = f'mesh {mesh.id}' + assert (mesh_key, 'x') in df.columns + assert (mesh_key, 'y') in df.columns + # Should NOT have a z column for a 2D mesh + assert (mesh_key, 'z') not in df.columns + assert len(df) == data_size + + +def test_mesh_filter_dataframe_regular_1d(): + """Test MeshFilter.get_pandas_dataframe with a 1D RegularMesh.""" + mesh = openmc.RegularMesh() + mesh.lower_left = [0] + mesh.upper_right = [5] + mesh.dimension = [5] + + f = openmc.MeshFilter(mesh) + data_size = 5 + df = f.get_pandas_dataframe(data_size, stride=1) + + mesh_key = f'mesh {mesh.id}' + assert (mesh_key, 'x') in df.columns + assert (mesh_key, 'y') not in df.columns + assert (mesh_key, 'z') not in df.columns + assert len(df) == data_size + assert list(df[(mesh_key, 'x')]) == [1, 2, 3, 4, 5] + + +def test_mesh_filter_dataframe_cylindrical(): + """Test MeshFilter.get_pandas_dataframe with a CylindricalMesh.""" + mesh = openmc.CylindricalMesh( + r_grid=[0.0, 1.0, 2.0], + phi_grid=[0.0, math.pi], + z_grid=[0.0, 5.0, 10.0] + ) + + f = openmc.MeshFilter(mesh) + nr, nphi, nz = mesh.dimension # 2, 1, 2 + data_size = nr * nphi * nz + df = f.get_pandas_dataframe(data_size, stride=1) + + mesh_key = f'mesh {mesh.id}' + assert (mesh_key, 'r') in df.columns + assert (mesh_key, 'phi') in df.columns + assert (mesh_key, 'z') in df.columns + # Should NOT have x, y columns + assert (mesh_key, 'x') not in df.columns + assert (mesh_key, 'y') not in df.columns + assert len(df) == data_size + # r varies fastest + assert list(df[(mesh_key, 'r')][:nr]) == [1, 2] + + +def test_mesh_filter_dataframe_spherical(): + """Test MeshFilter.get_pandas_dataframe with a SphericalMesh.""" + mesh = openmc.SphericalMesh( + r_grid=[0.0, 1.0, 2.0, 3.0], + theta_grid=[0, math.pi], + phi_grid=[0, 2 * math.pi] + ) + + f = openmc.MeshFilter(mesh) + nr, ntheta, nphi = mesh.dimension # 3, 1, 1 + data_size = nr * ntheta * nphi + df = f.get_pandas_dataframe(data_size, stride=1) + + mesh_key = f'mesh {mesh.id}' + assert (mesh_key, 'r') in df.columns + assert (mesh_key, 'theta') in df.columns + assert (mesh_key, 'phi') in df.columns + assert (mesh_key, 'x') not in df.columns + assert len(df) == data_size + + +def test_mesh_filter_dataframe_rectilinear(): + """Test MeshFilter.get_pandas_dataframe with a RectilinearMesh.""" + mesh = openmc.RectilinearMesh() + mesh.x_grid = [0.0, 1.0, 2.0] + mesh.y_grid = [0.0, 1.0, 2.0, 3.0] + mesh.z_grid = [0.0, 5.0] + + f = openmc.MeshFilter(mesh) + nx, ny, nz = mesh.dimension # 2, 3, 1 + data_size = nx * ny * nz + df = f.get_pandas_dataframe(data_size, stride=1) + + mesh_key = f'mesh {mesh.id}' + assert (mesh_key, 'x') in df.columns + assert (mesh_key, 'y') in df.columns + assert (mesh_key, 'z') in df.columns + assert len(df) == data_size From 3ff6b59a491a11682b9cc9c9748b22005254e67d Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Wed, 25 Feb 2026 06:44:30 +0100 Subject: [PATCH 13/38] Allow Mesh.from_domain to use bounding boxes directly (#3828) --- openmc/mesh.py | 63 ++++++++++++++--------- tests/unit_tests/test_mesh_from_domain.py | 11 ++++ 2 files changed, 49 insertions(+), 25 deletions(-) diff --git a/openmc/mesh.py b/openmc/mesh.py index 946b90fbf6..3f0802d6be 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -1188,7 +1188,7 @@ class RegularMesh(StructuredMesh): @classmethod def from_domain( cls, - domain: HasBoundingBox, + domain: HasBoundingBox | BoundingBox, dimension: Sequence[int] | int = 1000, mesh_id: int | None = None, name: str = '' @@ -1197,10 +1197,12 @@ class RegularMesh(StructuredMesh): Parameters ---------- - domain : HasBoundingBox + domain : HasBoundingBox | openmc.BoundingBox The object passed in will be used as a template for this mesh. The bounding box of the property of the object passed will be used to - set the lower_left and upper_right and of the mesh instance + set the lower_left and upper_right and of the mesh instance. + Alternatively, a :class:`openmc.BoundingBox` can be passed + directly. dimension : Iterable of int | int The number of mesh cells in total or number of mesh cells in each direction (x, y, z). If a single integer is provided, the domain @@ -1217,21 +1219,26 @@ class RegularMesh(StructuredMesh): RegularMesh instance """ - if not hasattr(domain, 'bounding_box'): - raise TypeError("Domain must have a bounding_box property") + if isinstance(domain, BoundingBox): + bb = domain + elif hasattr(domain, 'bounding_box'): + bb = domain.bounding_box + else: + raise TypeError("Domain must be a BoundingBox or have a " + "bounding_box property") mesh = cls(mesh_id=mesh_id, name=name) - mesh.lower_left = domain.bounding_box[0] - mesh.upper_right = domain.bounding_box[1] + mesh.lower_left = bb[0] + mesh.upper_right = bb[1] if isinstance(dimension, int): cv.check_greater_than("dimension", dimension, 1, equality=True) # If a single integer is provided, divide the domain into that many # mesh cells with roughly equal lengths in each direction - ideal_cube_volume = domain.bounding_box.volume / dimension + ideal_cube_volume = bb.volume / dimension ideal_cube_size = ideal_cube_volume ** (1 / 3) dimension = [ max(1, int(round(side / ideal_cube_size))) - for side in domain.bounding_box.width + for side in bb.width ] mesh.dimension = dimension @@ -1904,7 +1911,7 @@ class CylindricalMesh(StructuredMesh): @classmethod def from_domain( cls, - domain: HasBoundingBox, + domain: HasBoundingBox | BoundingBox, dimension: Sequence[int] = (10, 10, 10), mesh_id: int | None = None, phi_grid_bounds: Sequence[float] = (0.0, 2*pi), @@ -1915,10 +1922,11 @@ class CylindricalMesh(StructuredMesh): Parameters ---------- - domain : HasBoundingBox + domain : HasBoundingBox | openmc.BoundingBox The object passed in will be used as a template for this mesh. The bounding box of the property of the object passed will be used to - set the r_grid, z_grid ranges. + set the r_grid, z_grid ranges. Alternatively, a + :class:`openmc.BoundingBox` can be passed directly. dimension : Iterable of int The number of equally spaced mesh cells in each direction (r_grid, phi_grid, z_grid) @@ -1939,11 +1947,13 @@ class CylindricalMesh(StructuredMesh): CylindricalMesh instance """ - if not hasattr(domain, 'bounding_box'): - raise TypeError("Domain must have a bounding_box property") - - # loaded once to avoid recalculating bounding box - cached_bb = domain.bounding_box + if isinstance(domain, BoundingBox): + cached_bb = domain + elif hasattr(domain, 'bounding_box'): + cached_bb = domain.bounding_box + else: + raise TypeError("Domain must be a BoundingBox or have a " + "bounding_box property") if enclose_domain: outer_radius = 0.5 * np.linalg.norm(cached_bb.width[:2]) @@ -2290,7 +2300,7 @@ class SphericalMesh(StructuredMesh): @classmethod def from_domain( cls, - domain: HasBoundingBox, + domain: HasBoundingBox | BoundingBox, dimension: Sequence[int] = (10, 10, 10), mesh_id: int | None = None, phi_grid_bounds: Sequence[float] = (0.0, 2*pi), @@ -2302,10 +2312,11 @@ class SphericalMesh(StructuredMesh): Parameters ---------- - domain : HasBoundingBox + domain : HasBoundingBox | openmc.BoundingBox The object passed in will be used as a template for this mesh. The bounding box of the property of the object passed will be used to - set the r_grid, phi_grid, and theta_grid ranges. + set the r_grid, phi_grid, and theta_grid ranges. Alternatively, a + :class:`openmc.BoundingBox` can be passed directly. dimension : Iterable of int The number of equally spaced mesh cells in each direction (r_grid, phi_grid, theta_grid). Spacing is in angular space (radians) for @@ -2330,11 +2341,13 @@ class SphericalMesh(StructuredMesh): SphericalMesh instance """ - if not hasattr(domain, 'bounding_box'): - raise TypeError("Domain must have a bounding_box property") - - # loaded once to avoid recalculating bounding box - cached_bb = domain.bounding_box + if isinstance(domain, BoundingBox): + cached_bb = domain + elif hasattr(domain, 'bounding_box'): + cached_bb = domain.bounding_box + else: + raise TypeError("Domain must be a BoundingBox or have a " + "bounding_box property") if enclose_domain: outer_radius = 0.5 * np.linalg.norm(cached_bb.width) diff --git a/tests/unit_tests/test_mesh_from_domain.py b/tests/unit_tests/test_mesh_from_domain.py index 5b1173126f..2b02921b5e 100644 --- a/tests/unit_tests/test_mesh_from_domain.py +++ b/tests/unit_tests/test_mesh_from_domain.py @@ -16,6 +16,17 @@ def test_reg_mesh_from_cell(): assert np.array_equal(mesh.upper_right, cell.bounding_box[1]) +def test_reg_mesh_from_bounding_box(): + """Tests a RegularMesh can be made from a BoundingBox directly.""" + bb = openmc.BoundingBox([-8, -7, -5], [12, 13, 15]) + + mesh = openmc.RegularMesh.from_domain(domain=bb, dimension=[7, 11, 13]) + assert isinstance(mesh, openmc.RegularMesh) + assert np.array_equal(mesh.dimension, (7, 11, 13)) + assert np.array_equal(mesh.lower_left, bb[0]) + assert np.array_equal(mesh.upper_right, bb[1]) + + def test_cylindrical_mesh_from_cell(): """Tests a CylindricalMesh can be made from a Cell and the specified dimensions are propagated through.""" From 5a85bd92f2d6f5054829cfbac7d660b734cca40f Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Wed, 25 Feb 2026 11:08:04 -0600 Subject: [PATCH 14/38] C++ formatting suggestions on PRs (#3829) --- .github/workflows/format-check.yml | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/.github/workflows/format-check.yml b/.github/workflows/format-check.yml index cef14ca2c8..42b28b2cdc 100644 --- a/.github/workflows/format-check.yml +++ b/.github/workflows/format-check.yml @@ -5,6 +5,12 @@ on: workflow_dispatch: pull_request: + types: + - opened + - synchronize + - reopened + - labeled + - unlabeled branches: - develop - master @@ -12,6 +18,9 @@ on: jobs: cpp-linter: runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write steps: - uses: actions/checkout@v4 - uses: cpp-linter/cpp-linter-action@v2 @@ -23,10 +32,29 @@ jobs: files-changed-only: true tidy-checks: '-*' version: '15' # clang-format version + format-review: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'cpp-format-suggest') }} + passive-reviews: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'cpp-format-suggest') }} file-annotations: true step-summary: true extensions: 'cpp,h' + - name: Comment with suggestion instructions + if: steps.linter.outputs.checks-failed > 0 && !contains(github.event.pull_request.labels.*.name, 'cpp-format-suggest') + uses: actions/github-script@v7 + with: + script: | + const {owner, repo} = context.repo; + const issue_number = context.payload.pull_request.number; + await github.rest.issues.createComment({ + owner, + repo, + issue_number, + body: "C++ formatting checks failed. Add the `cpp-format-suggest` label to this PR for inline formatting suggestions on the next run." + }); + - name: Failure Check if: steps.linter.outputs.checks-failed > 0 - run: echo "Some files failed the formatting check! See job summary and file annotations for more info" && exit 1 + run: | + echo "Some files failed the formatting check." + echo "See job summary and file annotations for details." + exit 1 From c0427dd40ad419e2cde38dfc8491d6437f64caee Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Wed, 25 Feb 2026 22:01:33 +0200 Subject: [PATCH 15/38] Resolve conflict with weight windows and global russian roulette (#3751) Co-authored-by: Patrick Shriwise --- include/openmc/physics_common.h | 4 + include/openmc/weight_windows.h | 33 +- src/physics.cpp | 28 +- src/physics_common.cpp | 16 + src/physics_mg.cpp | 28 +- src/random_ray/flat_source_domain.cpp | 2 +- src/settings.cpp | 7 + src/weight_windows.cpp | 234 +++++----- .../survival_biasing/__init__.py | 0 .../survival_biasing/inputs_true.dat | 68 +++ .../survival_biasing/results_true.dat | 1 + .../weightwindows/survival_biasing/test.py | 81 ++++ .../weightwindows/survival_biasing/ww_n.txt | 400 ++++++++++++++++++ 13 files changed, 748 insertions(+), 154 deletions(-) create mode 100644 tests/regression_tests/weightwindows/survival_biasing/__init__.py create mode 100644 tests/regression_tests/weightwindows/survival_biasing/inputs_true.dat create mode 100644 tests/regression_tests/weightwindows/survival_biasing/results_true.dat create mode 100644 tests/regression_tests/weightwindows/survival_biasing/test.py create mode 100644 tests/regression_tests/weightwindows/survival_biasing/ww_n.txt diff --git a/include/openmc/physics_common.h b/include/openmc/physics_common.h index e38a3c7f88..b0e395c1ea 100644 --- a/include/openmc/physics_common.h +++ b/include/openmc/physics_common.h @@ -13,5 +13,9 @@ namespace openmc { //! \param[in] weight_survive Weight assigned to particles that survive void russian_roulette(Particle& p, double weight_survive); +//! \brief Performs the global russian roulette operation on a particle +//! \param[in,out] p Particle object +void apply_russian_roulette(Particle& p); + } // namespace openmc #endif // OPENMC_PHYSICS_COMMON_H diff --git a/include/openmc/weight_windows.h b/include/openmc/weight_windows.h index 97abcfe82e..42846f9d14 100644 --- a/include/openmc/weight_windows.h +++ b/include/openmc/weight_windows.h @@ -25,17 +25,6 @@ enum class WeightWindowUpdateMethod { MAGIC, FW_CADIS }; constexpr double DEFAULT_WEIGHT_CUTOFF {1.0e-38}; // default low weight cutoff -//============================================================================== -// Non-member functions -//============================================================================== - -//! Apply weight windows to a particle -//! \param[in] p Particle to apply weight windows to -void apply_weight_windows(Particle& p); - -//! Free memory associated with weight windows -void free_memory_weight_windows(); - //============================================================================== // Global variables //============================================================================== @@ -137,7 +126,7 @@ public: //! Retrieve the weight window for a particle //! \param[in] p Particle to get weight window for - WeightWindow get_weight_window(const Particle& p) const; + std::pair get_weight_window(const Particle& p) const; std::array bounds_size() const; @@ -236,6 +225,26 @@ public: double ratio_ {5.0}; // search_weight_window(const Particle& p); + //! Finalize variance reduction objects after all inputs have been read void finalize_variance_reduction(); diff --git a/src/physics.cpp b/src/physics.cpp index c9737205ce..72b4a66119 100644 --- a/src/physics.cpp +++ b/src/physics.cpp @@ -64,8 +64,17 @@ void collision(Particle& p) fatal_error("Unsupported particle PDG for collision sampling."); } - if (settings::weight_window_checkpoint_collision) - apply_weight_windows(p); + if (settings::weight_windows_on) { + auto [ww_found, ww] = search_weight_window(p); + if (!ww_found && p.type() == ParticleType::neutron()) { + // if the weight window is not valid, apply russian roulette for neutrons + // (regardless of weight window collision checkpoint setting) + apply_russian_roulette(p); + } else if (settings::weight_window_checkpoint_collision) { + // if collision checkpointing is on, apply weight window + apply_weight_window(p, ww); + } + } // Kill particle if energy falls below cutoff int type = p.type().transport_index(); @@ -156,18 +165,9 @@ void sample_neutron_reaction(Particle& p) advance_prn_seed(data::nuclides.size(), &p.seeds(STREAM_URR_PTABLE)); } - // Play russian roulette if survival biasing is turned on - if (settings::survival_biasing) { - // if survival normalization is on, use normalized weight cutoff and - // normalized weight survive - if (settings::survival_normalization) { - if (p.wgt() < settings::weight_cutoff * p.wgt_born()) { - russian_roulette(p, settings::weight_survive * p.wgt_born()); - } - } else if (p.wgt() < settings::weight_cutoff) { - russian_roulette(p, settings::weight_survive); - } - } + // Play russian roulette if there are no weight windows + if (!settings::weight_windows_on) + apply_russian_roulette(p); } void create_fission_sites(Particle& p, int i_nuclide, const Reaction& rx) diff --git a/src/physics_common.cpp b/src/physics_common.cpp index e25ae6b97a..10760ce2da 100644 --- a/src/physics_common.cpp +++ b/src/physics_common.cpp @@ -18,4 +18,20 @@ void russian_roulette(Particle& p, double weight_survive) } } +void apply_russian_roulette(Particle& p) +{ + // Exit if survival biasing is turned off + if (!settings::survival_biasing) + return; + + // if survival normalization is on, use normalized weight cutoff and + // normalized weight survive + if (settings::survival_normalization) { + if (p.wgt() < settings::weight_cutoff * p.wgt_born()) { + russian_roulette(p, settings::weight_survive * p.wgt_born()); + } + } else if (p.wgt() < settings::weight_cutoff) { + russian_roulette(p, settings::weight_survive); + } +} } // namespace openmc diff --git a/src/physics_mg.cpp b/src/physics_mg.cpp index 32ae10a609..6991c73655 100644 --- a/src/physics_mg.cpp +++ b/src/physics_mg.cpp @@ -32,8 +32,17 @@ void collision_mg(Particle& p) // Sample the reaction type sample_reaction(p); - if (settings::weight_window_checkpoint_collision) - apply_weight_windows(p); + if (settings::weight_windows_on) { + auto [ww_found, ww] = search_weight_window(p); + if (!ww_found && p.type() == ParticleType::neutron()) { + // if the weight window is not valid, apply russian roulette + // (regardless of weight window collision checkpoint setting) + apply_russian_roulette(p); + } else if (settings::weight_window_checkpoint_collision) { + // if collision checkpointing is on, apply weight window + apply_weight_window(p, ww); + } + } // Display information about collision if ((settings::verbosity >= 10) || p.trace()) { @@ -67,18 +76,9 @@ void sample_reaction(Particle& p) // Sample a scattering event to determine the energy of the exiting neutron scatter(p); - // Play Russian roulette if survival biasing is turned on - if (settings::survival_biasing) { - // if survival normalization is applicable, use normalized weight cutoff and - // normalized weight survive - if (settings::survival_normalization) { - if (p.wgt() < settings::weight_cutoff * p.wgt_born()) { - russian_roulette(p, settings::weight_survive * p.wgt_born()); - } - } else if (p.wgt() < settings::weight_cutoff) { - russian_roulette(p, settings::weight_survive); - } - } + // Play russian roulette if there are no weight windows + if (!settings::weight_windows_on) + apply_russian_roulette(p); } void scatter(Particle& p) diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index b881ced716..1a6e7c0be4 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -843,7 +843,7 @@ void FlatSourceDomain::output_to_vtk() const voxel_positions[z * Ny * Nx + y * Nx + x] = sample; if (variance_reduction::weight_windows.size() == 1) { - WeightWindow ww = + auto [ww_found, ww] = variance_reduction::weight_windows[0]->get_weight_window(p); float weight = ww.lower_weight; weight_windows[z * Ny * Nx + y * Nx + x] = weight; diff --git a/src/settings.cpp b/src/settings.cpp index b6a9ab9537..ee8edd4a52 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -1266,6 +1266,13 @@ void read_settings_xml(pugi::xml_node root) } } + if (weight_windows_on) { + if (!weight_window_checkpoint_surface && + !weight_window_checkpoint_collision) + fatal_error( + "Weight Windows are enabled but there are no valid checkpoints."); + } + if (check_for_node(root, "use_decay_photons")) { settings::use_decay_photons = get_node_value_bool(root, "use_decay_photons"); diff --git a/src/weight_windows.cpp b/src/weight_windows.cpp index c051691320..c00872e564 100644 --- a/src/weight_windows.cpp +++ b/src/weight_windows.cpp @@ -44,108 +44,6 @@ openmc::vector> weight_windows_generators; } // namespace variance_reduction -//============================================================================== -// Non-member functions -//============================================================================== - -void apply_weight_windows(Particle& p) -{ - if (!settings::weight_windows_on) - return; - - // WW on photon and neutron only - if (!p.type().is_neutron() && !p.type().is_photon()) - return; - - // skip dead or no energy - if (p.E() <= 0 || !p.alive()) - return; - - bool in_domain = false; - // TODO: this is a linear search - should do something more clever - WeightWindow weight_window; - for (const auto& ww : variance_reduction::weight_windows) { - weight_window = ww->get_weight_window(p); - if (weight_window.is_valid()) - break; - } - - // If particle has not yet had its birth weight window value set, set it to - // the current weight window (or 1.0 if not born in a weight window). - if (p.wgt_ww_born() == -1.0) { - if (weight_window.is_valid()) { - p.wgt_ww_born() = - (weight_window.lower_weight + weight_window.upper_weight) / 2; - } else { - p.wgt_ww_born() = 1.0; - } - } - - // particle is not in any of the ww domains, do nothing - if (!weight_window.is_valid()) - return; - - // Normalize weight windows based on particle's starting weight - // and the value of the weight window the particle was born in. - weight_window.scale(p.wgt_born() / p.wgt_ww_born()); - - // get the paramters - double weight = p.wgt(); - - // first check to see if particle should be killed for weight cutoff - if (p.wgt() < weight_window.weight_cutoff) { - p.wgt() = 0.0; - return; - } - - // check if particle is far above current weight window - // only do this if the factor is not already set on the particle and a - // maximum lower bound ratio is specified - if (p.ww_factor() == 0.0 && weight_window.max_lb_ratio > 1.0 && - p.wgt() > weight_window.lower_weight * weight_window.max_lb_ratio) { - p.ww_factor() = - p.wgt() / (weight_window.lower_weight * weight_window.max_lb_ratio); - } - - // move weight window closer to the particle weight if needed - if (p.ww_factor() > 1.0) - weight_window.scale(p.ww_factor()); - - // if particle's weight is above the weight window split until they are within - // the window - if (weight > weight_window.upper_weight) { - // do not further split the particle if above the limit - if (p.n_split() >= settings::max_history_splits) - return; - - double n_split = std::ceil(weight / weight_window.upper_weight); - double max_split = weight_window.max_split; - n_split = std::min(n_split, max_split); - - p.n_split() += n_split; - - // Create secondaries and divide weight among all particles - int i_split = std::round(n_split); - for (int l = 0; l < i_split - 1; l++) { - p.split(weight / n_split); - } - // remaining weight is applied to current particle - p.wgt() = weight / n_split; - - } else if (weight <= weight_window.lower_weight) { - // if the particle weight is below the window, play Russian roulette - double weight_survive = - std::min(weight * weight_window.max_split, weight_window.survival_weight); - russian_roulette(p, weight_survive); - } // else particle is in the window, continue as normal -} - -void free_memory_weight_windows() -{ - variance_reduction::ww_map.clear(); - variance_reduction::weight_windows.clear(); -} - //============================================================================== // WeightWindowSettings implementation //============================================================================== @@ -375,27 +273,28 @@ void WeightWindows::set_mesh(const Mesh* mesh) set_mesh(model::mesh_map[mesh->id_]); } -WeightWindow WeightWindows::get_weight_window(const Particle& p) const +std::pair WeightWindows::get_weight_window( + const Particle& p) const { // check for particle type if (particle_type_ != p.type()) { - return {}; + return {false, {}}; } + // particle energy + double E = p.E(); + + // check to make sure energy is in range, expects sorted energy values + if (E < energy_bounds_.front() || E > energy_bounds_.back()) + return {false, {}}; + // Get mesh index for particle's position const auto& mesh = this->mesh(); int mesh_bin = mesh->get_bin(p.r()); // particle is outside the weight window mesh if (mesh_bin < 0) - return {}; - - // particle energy - double E = p.E(); - - // check to make sure energy is in range, expects sorted energy values - if (E < energy_bounds_.front() || E > energy_bounds_.back()) - return {}; + return {false, {}}; // get the mesh bin in energy group int energy_bin = @@ -410,7 +309,7 @@ WeightWindow WeightWindows::get_weight_window(const Particle& p) const ww.max_lb_ratio = max_lb_ratio_; ww.max_split = max_split_; ww.weight_cutoff = weight_cutoff_; - return ww; + return {true, ww}; } std::array WeightWindows::bounds_size() const @@ -1023,6 +922,115 @@ void WeightWindowsGenerator::update() const // Non-member functions //============================================================================== +std::pair search_weight_window(const Particle& p) +{ + // TODO: this is a linear search - should do something more clever + for (const auto& ww : variance_reduction::weight_windows) { + auto [ww_found, weight_window] = ww->get_weight_window(p); + if (ww_found) + return {true, weight_window}; + } + return {false, {}}; +} + +void apply_weight_windows(Particle& p) +{ + if (!settings::weight_windows_on) + return; + + // WW on photon and neutron only + if (!p.type().is_neutron() && !p.type().is_photon()) + return; + + // skip dead or no energy + if (p.E() <= 0 || !p.alive()) + return; + + auto [ww_found, ww] = search_weight_window(p); + if (ww_found && ww.is_valid()) { + apply_weight_window(p, ww); + } else { + if (p.wgt_ww_born() == -1.0) + p.wgt_ww_born() = 1.0; + } +} + +void apply_weight_window(Particle& p, WeightWindow weight_window) +{ + if (!weight_window.is_valid()) + return; + + // skip dead or no energy + if (p.E() <= 0 || !p.alive()) + return; + + // If particle has not yet had its birth weight window value set, set it to + // the current weight window. + if (p.wgt_ww_born() == -1.0) + p.wgt_ww_born() = + (weight_window.lower_weight + weight_window.upper_weight) / 2; + + // Normalize weight windows based on particle's starting weight + // and the value of the weight window the particle was born in. + weight_window.scale(p.wgt_born() / p.wgt_ww_born()); + + // get the paramters + double weight = p.wgt(); + + // first check to see if particle should be killed for weight cutoff + if (p.wgt() < weight_window.weight_cutoff) { + p.wgt() = 0.0; + return; + } + + // check if particle is far above current weight window + // only do this if the factor is not already set on the particle and a + // maximum lower bound ratio is specified + if (p.ww_factor() == 0.0 && weight_window.max_lb_ratio > 1.0 && + p.wgt() > weight_window.lower_weight * weight_window.max_lb_ratio) { + p.ww_factor() = + p.wgt() / (weight_window.lower_weight * weight_window.max_lb_ratio); + } + + // move weight window closer to the particle weight if needed + if (p.ww_factor() > 1.0) + weight_window.scale(p.ww_factor()); + + // if particle's weight is above the weight window split until they are within + // the window + if (weight > weight_window.upper_weight) { + // do not further split the particle if above the limit + if (p.n_split() >= settings::max_history_splits) + return; + + double n_split = std::ceil(weight / weight_window.upper_weight); + double max_split = weight_window.max_split; + n_split = std::min(n_split, max_split); + + p.n_split() += n_split; + + // Create secondaries and divide weight among all particles + int i_split = std::round(n_split); + for (int l = 0; l < i_split - 1; l++) { + p.split(weight / n_split); + } + // remaining weight is applied to current particle + p.wgt() = weight / n_split; + + } else if (weight <= weight_window.lower_weight) { + // if the particle weight is below the window, play Russian roulette + double weight_survive = + std::min(weight * weight_window.max_split, weight_window.survival_weight); + russian_roulette(p, weight_survive); + } // else particle is in the window, continue as normal +} + +void free_memory_weight_windows() +{ + variance_reduction::ww_map.clear(); + variance_reduction::weight_windows.clear(); +} + void finalize_variance_reduction() { for (const auto& wwg : variance_reduction::weight_windows_generators) { diff --git a/tests/regression_tests/weightwindows/survival_biasing/__init__.py b/tests/regression_tests/weightwindows/survival_biasing/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regression_tests/weightwindows/survival_biasing/inputs_true.dat b/tests/regression_tests/weightwindows/survival_biasing/inputs_true.dat new file mode 100644 index 0000000000..d5aa135d47 --- /dev/null +++ b/tests/regression_tests/weightwindows/survival_biasing/inputs_true.dat @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + fixed source + 50 + 5 + + + + 0.01 1.0 + + + + + + + 14100000.0 1.0 + + + true + + 1 + neutron + 0.46135961568957096 0.2874485390202603 0.13256013950494605 0.052765209609807295 0.020958440832685638 0.006317250035749876 0.002627037175682506 0.00030032343592437284 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.4611178493390162 0.28624862560842024 0.13999870622621136 0.05508479408959756 0.018281241958254993 0.0055577764872361555 0.0015265432226293397 0.00024298772352636966 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.46294957913063317 0.2857734367546051 0.13365623190336945 0.05034742166227103 0.017525851812913266 0.005096725451851077 0.0026297640951531403 0.00033732424392026144 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.45772598750472554 0.281494921471495 0.13604397962762246 0.054792881472295364 0.019802376898755525 0.0073351745579128495 0.002067392946066426 9.529474085926143e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.4739762983490318 0.28419061537046764 0.12757509901507888 0.0516920293019733 0.01919167874787235 0.007593035964707848 0.0017488176314107277 9.678267909681988e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.47560199098558276 0.27728389146956994 0.12760802572535623 0.05251965811713552 0.022498960644951632 0.01063372459325151 0.004242071054914633 0.00045603112202665183 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.47659199471934693 0.28886666944252215 0.1258637573398161 0.05818862375955657 0.020476313720744762 0.007143193244018263 0.0022364083022515273 0.0003953123781408474 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.4693487369606823 0.27180862139490125 0.12833455570423483 0.055146743559938344 0.020667403529470333 0.007891508723137146 0.001643551705407358 0.0006501416781353278 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.4538724931023337 0.2824491421084296 0.1275341706351566 0.055027501141893705 0.02305114640508087 0.008599303158607198 0.0025082611194161804 0.000518530311231696 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.45258444753220123 0.26839102466484255 0.12991633918797083 0.05442263709947767 0.019695895223471486 0.005353204961887518 0.0015074698293840157 0.0001598680898180058 8.872047880811405e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.4482467985982804 0.2813276470183359 0.13449860790768647 0.056103278190533436 0.026274320334196962 0.006884071908429303 0.0033099390738735458 0.0005337454397674922 0.0001386380399465575 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.4875748950139487 0.2908586705316248 0.14212060658392278 0.06068108009637272 0.019467970468789477 0.00637102427946399 0.0028510425266191687 0.0012184421778115488 0.00029404541567146187 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.5 0.2852101551297824 0.13622307172321643 0.058302519884339946 0.023124734915152625 0.007401527839638796 0.002527635652743992 0.0008509612315162482 0.00018213334574554768 7.815977984795461e-06 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.47706397688984176 0.28313436859495966 0.13170624744221976 0.0525453512766549 0.021111753158319105 0.0067451713955609645 0.003204270539718037 0.0010308107242263192 0.0002759210144794204 0.00013727805365387318 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.48641988659284513 0.2816173231471242 0.131541850061199 0.054793200248327956 0.016517701691156576 0.006757591781856257 0.0025425350750908644 0.0006383278085839443 2.279421641064226e-05 0.0003597432472224371 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.4307755159245372 0.2652713784080849 0.12753857957535653 0.05470396852881577 0.02049817309420563 0.00566559741247352 0.0007213846765465147 6.848024591311009e-05 -1.0 8.609308814924014e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.43631324024956025 0.2639077825530567 0.13368737962742414 0.05213531603720763 0.020628860469743833 0.008268384844678654 0.0028599221013934375 0.00013059757129982714 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.45926287238219 0.2635824148883888 0.13104280996799478 0.055379812186041905 0.019255042561941074 0.007252095690246872 0.0018026593488140996 0.0003215836458542421 9.14840176000331e-06 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.4574378998400604 0.28714971179927723 0.1363507911051337 0.05010315954654347 0.017963278989571074 0.0058998176297971605 0.0021393135666168 0.00015677575033083482 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.4821442782978812 0.27704132233083506 0.14149074035662307 0.05546042815834048 0.016800649382269998 0.004096804603054233 0.0019161108398578026 0.0005289600253155307 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 + 2.306798078447855 1.4372426951013015 0.6628006975247303 0.2638260480490365 0.10479220416342819 0.03158625017874938 0.013135185878412529 0.0015016171796218643 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.3055892466950807 1.4312431280421012 0.6999935311310568 0.2754239704479878 0.09140620979127496 0.027788882436180776 0.007632716113146698 0.0012149386176318483 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.314747895653166 1.4288671837730256 0.6682811595168472 0.25173710831135515 0.08762925906456634 0.025483627259255386 0.013148820475765701 0.0016866212196013073 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.288629937523628 1.407474607357475 0.6802198981381123 0.2739644073614768 0.09901188449377762 0.036675872789564246 0.01033696473033213 0.00047647370429630714 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.369881491745159 1.4209530768523382 0.6378754950753944 0.2584601465098665 0.09595839373936176 0.03796517982353924 0.008744088157053638 0.00048391339548409945 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.3780099549279137 1.3864194573478497 0.6380401286267812 0.26259829058567763 0.11249480322475816 0.053168622966257545 0.021210355274573163 0.0022801556101332593 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.382959973596735 1.4443333472126108 0.6293187866990806 0.29094311879778284 0.10238156860372381 0.035715966220091315 0.011182041511257637 0.001976561890704237 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.3467436848034113 1.3590431069745064 0.6416727785211741 0.2757337177996917 0.10333701764735166 0.03945754361568573 0.00821775852703679 0.0032507083906766388 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.2693624655116684 1.412245710542148 0.637670853175783 0.27513750570946854 0.11525573202540434 0.04299651579303599 0.012541305597080901 0.00259265155615848 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.262922237661006 1.3419551233242126 0.6495816959398542 0.2721131854973884 0.09847947611735743 0.02676602480943759 0.007537349146920078 0.000799340449090029 0.0004436023940405703 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.2412339929914022 1.4066382350916795 0.6724930395384323 0.28051639095266717 0.1313716016709848 0.03442035954214652 0.016549695369367727 0.0026687271988374613 0.0006931901997327875 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.4378744750697434 1.4542933526581239 0.7106030329196139 0.3034054004818636 0.09733985234394739 0.03185512139731995 0.014255212633095843 0.006092210889057744 0.0014702270783573093 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.5 1.4260507756489118 0.6811153586160822 0.2915125994216997 0.11562367457576313 0.03700763919819398 0.012638178263719959 0.004254806157581241 0.0009106667287277384 3.9079889923977307e-05 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.385319884449209 1.4156718429747983 0.6585312372110987 0.2627267563832745 0.10555876579159552 0.03372585697780482 0.016021352698590185 0.005154053621131596 0.001379605072397102 0.000686390268269366 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.432099432964226 1.4080866157356209 0.657709250305995 0.2739660012416398 0.08258850845578287 0.03378795890928128 0.012712675375454322 0.0031916390429197216 0.0001139710820532113 0.0017987162361121855 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.1538775796226863 1.3263568920404245 0.6376928978767826 0.27351984264407886 0.10249086547102815 0.028327987062367603 0.0036069233827325737 0.0003424012295655504 -5.0 0.0004304654407462007 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.181566201247801 1.3195389127652835 0.6684368981371207 0.2606765801860381 0.10314430234871916 0.041341924223393264 0.014299610506967188 0.0006529878564991358 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.29631436191095 1.317912074441944 0.6552140498399739 0.27689906093020955 0.09627521280970537 0.03626047845123436 0.009013296744070498 0.0016079182292712106 4.574200880001655e-05 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.287189499200302 1.435748558996386 0.6817539555256685 0.25051579773271737 0.08981639494785537 0.029499088148985803 0.010696567833083998 0.0007838787516541741 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 2.410721391489406 1.3852066116541752 0.7074537017831153 0.2773021407917024 0.08400324691134999 0.020484023015271167 0.009580554199289014 0.0026448001265776534 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 -5.0 + 3.0 + 10 + 1e-38 + + + 20 20 1 + 0.0 0.0 0.0 + 160.0 160.0 160.0 + + + true + true + + + + + 1 + + + 1 + flux + + + diff --git a/tests/regression_tests/weightwindows/survival_biasing/results_true.dat b/tests/regression_tests/weightwindows/survival_biasing/results_true.dat new file mode 100644 index 0000000000..b457a245c7 --- /dev/null +++ b/tests/regression_tests/weightwindows/survival_biasing/results_true.dat @@ -0,0 +1 @@ +386e507008ed3c72c6e1a101aafc01cfaaff2c0b10555558d1eab6332441f7b17264b55002d721151bac2f3e7c1a8aac4c5ed243f5270533d171850f985206ed \ No newline at end of file diff --git a/tests/regression_tests/weightwindows/survival_biasing/test.py b/tests/regression_tests/weightwindows/survival_biasing/test.py new file mode 100644 index 0000000000..92604e3382 --- /dev/null +++ b/tests/regression_tests/weightwindows/survival_biasing/test.py @@ -0,0 +1,81 @@ +import pytest +import numpy as np + +import openmc +from openmc.stats import Discrete, Point + +from tests.testing_harness import HashedPyAPITestHarness + + +@pytest.fixture +def model(): + # Material + w = openmc.Material(name='Tungsten') + w.add_element('W', 1.0) + w.set_density('g/cm3', 19.25) + + materials = openmc.Materials([w]) + + # Geometry surfaces + x0 = openmc.XPlane(x0=0.0, boundary_type='reflective') + x1 = openmc.XPlane(x0=160.0, boundary_type='vacuum') + y0 = openmc.YPlane(y0=0.0, boundary_type='reflective') + y1 = openmc.YPlane(y0=160.0, boundary_type='reflective') + z0 = openmc.ZPlane(z0=0.0, boundary_type='reflective') + z1 = openmc.ZPlane(z0=160.0, boundary_type='reflective') + + region = +x0 & -x1 & +y0 & -y1 & +z0 & -z1 + cell = openmc.Cell(region=region, fill=w) + root = openmc.Universe(cells=[cell]) + geometry = openmc.Geometry(root) + + # Source: planar on x=0, mono-directional along +x, 14.1 MeV neutrons + space = openmc.stats.CartesianIndependent( + openmc.stats.Discrete([0.01], [1.0]), + openmc.stats.Uniform(0.0, 160.0), + openmc.stats.Uniform(0.0, 160.0), + ) + angle = openmc.stats.Monodirectional((1.0, 0.0, 0.0)) + energy = openmc.stats.Discrete([14.1e6], [1.0]) + + source = openmc.Source(space=space, angle=angle, energy=energy) + + settings = openmc.Settings() + settings.run_mode = 'fixed source' + settings.batches = 5 + settings.particles = 50 + settings.source = source + + model = openmc.Model(geometry=geometry, materials=materials, settings=settings) + + # Mesh tally: 1 cm voxels, flux only + mesh = openmc.RegularMesh() + mesh.dimension = (20, 20, 1) + mesh.lower_left = (0.0, 0.0, 0.0) + mesh.upper_right = (160.0, 160.0, 160.0) + + mesh_filter = openmc.MeshFilter(mesh) + flux_tally = openmc.Tally(name='flux') + flux_tally.filters = [mesh_filter] + flux_tally.scores = ['flux'] + tallies = openmc.Tallies([flux_tally]) + model.tallies = tallies + + lower_ww_bounds = np.loadtxt('ww_n.txt') + + weight_windows = openmc.WeightWindows(mesh, + lower_ww_bounds, + upper_bound_ratio=5.0, + particle_type='neutron') + + model.settings.weight_windows = weight_windows + model.settings.weight_window_checkpoints = {'surface': True, + 'collision': True} + model.settings.survival_biasing = True + + return model + + +def test_weight_windows_with_survival_biasing(model): + harness = HashedPyAPITestHarness('statepoint.5.h5', model) + harness.main() diff --git a/tests/regression_tests/weightwindows/survival_biasing/ww_n.txt b/tests/regression_tests/weightwindows/survival_biasing/ww_n.txt new file mode 100644 index 0000000000..aea1ff1328 --- /dev/null +++ b/tests/regression_tests/weightwindows/survival_biasing/ww_n.txt @@ -0,0 +1,400 @@ +4.613596156895709566e-01 +4.611178493390161726e-01 +4.629495791306331709e-01 +4.577259875047255400e-01 +4.739762983490318216e-01 +4.756019909855827565e-01 +4.765919947193469342e-01 +4.693487369606823001e-01 +4.538724931023336850e-01 +4.525844475322012284e-01 +4.482467985982804271e-01 +4.875748950139486837e-01 +5.000000000000000000e-01 +4.770639768898417565e-01 +4.864198865928451299e-01 +4.307755159245372223e-01 +4.363132402495602524e-01 +4.592628723821899905e-01 +4.574378998400603913e-01 +4.821442782978812014e-01 +2.874485390202602964e-01 +2.862486256084202374e-01 +2.857734367546050924e-01 +2.814949214714950187e-01 +2.841906153704676363e-01 +2.772838914695699430e-01 +2.888666694425221504e-01 +2.718086213949012508e-01 +2.824491421084295850e-01 +2.683910246648425479e-01 +2.813276470183359024e-01 +2.908586705316247856e-01 +2.852101551297823723e-01 +2.831343685949596622e-01 +2.816173231471241767e-01 +2.652713784080849013e-01 +2.639077825530566912e-01 +2.635824148883887941e-01 +2.871497117992772297e-01 +2.770413223308350603e-01 +1.325601395049460507e-01 +1.399987062262113557e-01 +1.336562319033694490e-01 +1.360439796276224633e-01 +1.275750990150788799e-01 +1.276080257253562333e-01 +1.258637573398161125e-01 +1.283345557042348262e-01 +1.275341706351565962e-01 +1.299163391879708251e-01 +1.344986079076864738e-01 +1.421206065839227817e-01 +1.362230717232164323e-01 +1.317062474422197593e-01 +1.315418500611990060e-01 +1.275385795753565255e-01 +1.336873796274241355e-01 +1.310428099679947778e-01 +1.363507911051337063e-01 +1.414907403566230681e-01 +5.276520960980729535e-02 +5.508479408959755796e-02 +5.034742166227103299e-02 +5.479288147229536415e-02 +5.169202930197330098e-02 +5.251965811713552035e-02 +5.818862375955657223e-02 +5.514674355993834376e-02 +5.502750114189370462e-02 +5.442263709947767203e-02 +5.610327819053343573e-02 +6.068108009637272066e-02 +5.830251988433994559e-02 +5.254535127665489747e-02 +5.479320024832795566e-02 +5.470396852881576760e-02 +5.213531603720762686e-02 +5.537981218604190459e-02 +5.010315954654347148e-02 +5.546042815834047873e-02 +2.095844083268563751e-02 +1.828124195825499287e-02 +1.752585181291326649e-02 +1.980237689875552487e-02 +1.919167874787235106e-02 +2.249896064495163217e-02 +2.047631372074476194e-02 +2.066740352947033302e-02 +2.305114640508086968e-02 +1.969589522347148583e-02 +2.627432033419696208e-02 +1.946797046878947710e-02 +2.312473491515262478e-02 +2.111175315831910482e-02 +1.651770169115657563e-02 +2.049817309420563088e-02 +2.062886046974383297e-02 +1.925504256194107353e-02 +1.796327898957107358e-02 +1.680064938226999774e-02 +6.317250035749876098e-03 +5.557776487236155451e-03 +5.096725451851077254e-03 +7.335174557912849461e-03 +7.593035964707848043e-03 +1.063372459325150933e-02 +7.143193244018263000e-03 +7.891508723137145853e-03 +8.599303158607197670e-03 +5.353204961887517849e-03 +6.884071908429303423e-03 +6.371024279463989581e-03 +7.401527839638795750e-03 +6.745171395560964518e-03 +6.757591781856257113e-03 +5.665597412473520264e-03 +8.268384844678653561e-03 +7.252095690246871881e-03 +5.899817629797160512e-03 +4.096804603054233183e-03 +2.627037175682505905e-03 +1.526543222629339657e-03 +2.629764095153140288e-03 +2.067392946066426082e-03 +1.748817631410727689e-03 +4.242071054914632773e-03 +2.236408302251527251e-03 +1.643551705407358000e-03 +2.508261119416180414e-03 +1.507469829384015672e-03 +3.309939073873545759e-03 +2.851042526619168658e-03 +2.527635652743991969e-03 +3.204270539718037138e-03 +2.542535075090864363e-03 +7.213846765465147101e-04 +2.859922101393437468e-03 +1.802659348814099642e-03 +2.139313566616799812e-03 +1.916110839857802610e-03 +3.003234359243728432e-04 +2.429877235263696591e-04 +3.373242439202614419e-04 +9.529474085926143009e-05 +9.678267909681988429e-05 +4.560311220266518293e-04 +3.953123781408473961e-04 +6.501416781353277531e-04 +5.185303112316960250e-04 +1.598680898180058108e-04 +5.337454397674922446e-04 +1.218442177811548824e-03 +8.509612315162482432e-04 +1.030810724226319166e-03 +6.383278085839443235e-04 +6.848024591311008757e-05 +1.305975712998271444e-04 +3.215836458542421176e-04 +1.567757503308348204e-04 +5.289600253155306818e-04 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +8.872047880811405188e-05 +1.386380399465574921e-04 +2.940454156714618719e-04 +1.821333457455476831e-04 +2.759210144794204192e-04 +2.279421641064226044e-05 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +9.148401760003310179e-06 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +7.815977984795461318e-06 +1.372780536538731832e-04 +3.597432472224371168e-04 +8.609308814924013556e-05 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 From 2a38dc11ec17d07c84f0d17bf8bcf6e8be398adc Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:04:00 +0200 Subject: [PATCH 16/38] Do not fail CI when coveralls.io is not available. (#3835) --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b5b7f3a87e..d0382d8d6d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -210,6 +210,7 @@ jobs: parallel: true flag-name: C++ and Python path-to-lcov: coverage.lcov + fail-on-error: false coverage: needs: [filter-changes, main] @@ -222,6 +223,7 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} parallel-finished: true + fail-on-error: false ci-pass: needs: [filter-changes, main, coverage] From 54c8e3d6eb5e5b58e11695ae7f2101b96c7f065c Mon Sep 17 00:00:00 2001 From: John Tramm Date: Wed, 25 Feb 2026 17:12:18 -0600 Subject: [PATCH 17/38] Speedup CI and Improve Reproducibility Across Compilers (#3823) Co-authored-by: John Tramm Co-authored-by: Paul Romano --- AGENTS.md | 2 +- CMakeLists.txt | 38 ++++++++++++++++++ docs/source/devguide/tests.rst | 11 +++-- docs/source/usersguide/install.rst | 19 ++++++++- include/openmc/output.h | 2 + openmc/lib/__init__.py | 3 ++ src/output.cpp | 11 +++++ tests/conftest.py | 64 ++++++++++++++++++++++++++++++ tools/ci/gha-install.py | 7 +++- 9 files changed, 150 insertions(+), 7 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 575a693c73..d24eb68da6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -184,7 +184,7 @@ def test_my_feature(): harness.main() ``` -**Workflow**: Create `test.py` and `__init__.py` in `tests/regression_tests/my_test/`, run `pytest --update` to generate reference files (`inputs_true.dat`, `results_true.dat`, etc.), then verify with `pytest` without `--update`. Test results should be generated with a debug build (`-DCMAKE_BUILD_TYPE=Debug`) +**Workflow**: Create `test.py` and `__init__.py` in `tests/regression_tests/my_test/`, run `pytest --update` to generate reference files (`inputs_true.dat`, `results_true.dat`, etc.), then verify with `pytest` without `--update`. Test results should be generated with `-DOPENMC_ENABLE_STRICT_FP=on` to ensure reproducibility across platforms and optimization levels. **Critical**: When modifying OpenMC code, regenerate affected test references with `pytest --update` and commit updated reference files. diff --git a/CMakeLists.txt b/CMakeLists.txt index 5e6a647612..10867384b1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,6 +38,7 @@ option(OPENMC_USE_LIBMESH "Enable support for libMesh unstructured mesh tall option(OPENMC_USE_MPI "Enable MPI" OFF) option(OPENMC_USE_UWUW "Enable UWUW" OFF) option(OPENMC_FORCE_VENDORED_LIBS "Explicitly use submodules defined in 'vendor'" OFF) +option(OPENMC_ENABLE_STRICT_FP "Enable strict FP flags to improve test portability" OFF) message(STATUS "OPENMC_USE_OPENMP ${OPENMC_USE_OPENMP}") message(STATUS "OPENMC_BUILD_TESTS ${OPENMC_BUILD_TESTS}") @@ -48,6 +49,7 @@ message(STATUS "OPENMC_USE_LIBMESH ${OPENMC_USE_LIBMESH}") message(STATUS "OPENMC_USE_MPI ${OPENMC_USE_MPI}") message(STATUS "OPENMC_USE_UWUW ${OPENMC_USE_UWUW}") message(STATUS "OPENMC_FORCE_VENDORED_LIBS ${OPENMC_FORCE_VENDORED_LIBS}") +message(STATUS "OPENMC_ENABLE_STRICT_FP ${OPENMC_ENABLE_STRICT_FP}") # Warnings for deprecated options foreach(OLD_OPT IN ITEMS "openmp" "profile" "coverage" "dagmc" "libmesh") @@ -89,6 +91,19 @@ if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING "Choose the type of build" FORCE) endif() +#=============================================================================== +# When STRICT_FP is enabled, remove NDEBUG from RelWithDebInfo flags so that +# assert() remains active. CMake normally adds -DNDEBUG for both Release and +# RelWithDebInfo, which disables C/C++ assert() statements. +#=============================================================================== + +if(OPENMC_ENABLE_STRICT_FP) + foreach(FLAG_VAR CMAKE_CXX_FLAGS_RELWITHDEBINFO CMAKE_C_FLAGS_RELWITHDEBINFO) + string(REPLACE "-DNDEBUG" "" ${FLAG_VAR} "${${FLAG_VAR}}") + string(REPLACE "/DNDEBUG" "" ${FLAG_VAR} "${${FLAG_VAR}}") + endforeach() +endif() + #=============================================================================== # OpenMP for shared-memory parallelism (and GPU support some day!) #=============================================================================== @@ -193,6 +208,26 @@ endif() # Set compile/link flags based on which compiler is being used #=============================================================================== +# When OPENMC_ENABLE_STRICT_FP is enabled, disable compiler optimizations that change +# floating-point results relative to -O0, improving cross-platform and +# cross-optimization-level reproducibility for regression testing: +# -ffp-contract=off Prevents FMA contraction (fused multiply-add changes rounding) +# -fno-builtin Prevents replacing math function calls (pow, exp, log, etc.) +# with builtin versions that may differ from libm +# By default (OFF), the compiler is free to use all optimizations for best +# performance. +if(OPENMC_ENABLE_STRICT_FP) + include(CheckCXXCompilerFlag) + check_cxx_compiler_flag(-ffp-contract=off SUPPORTS_FP_CONTRACT_OFF) + if(SUPPORTS_FP_CONTRACT_OFF) + list(APPEND cxxflags -ffp-contract=off) + endif() + check_cxx_compiler_flag(-fno-builtin SUPPORTS_NO_BUILTIN) + if(SUPPORTS_NO_BUILTIN) + list(APPEND cxxflags -fno-builtin) + endif() +endif() + # Skip for Visual Studio which has its own configurations through GUI if(NOT MSVC) @@ -539,6 +574,9 @@ endif() if (OPENMC_ENABLE_COVERAGE) target_compile_definitions(libopenmc PRIVATE COVERAGEBUILD) endif() +if (OPENMC_ENABLE_STRICT_FP) + target_compile_definitions(libopenmc PRIVATE OPENMC_ENABLE_STRICT_FP) +endif() #=============================================================================== # openmc executable diff --git a/docs/source/devguide/tests.rst b/docs/source/devguide/tests.rst index f2e39441a8..8627cbde23 100644 --- a/docs/source/devguide/tests.rst +++ b/docs/source/devguide/tests.rst @@ -37,6 +37,9 @@ Prerequisites - Some tests require `NJOY `_ to preprocess cross section data. The test suite assumes that you have an ``njoy`` executable available on your :envvar:`PATH`. +- OpenMC should be compiled with ``-DOPENMC_ENABLE_STRICT_FP=on`` to ensure + reproducible floating-point results across platforms and optimization levels. + Without this flag, regression tests may not match reference values. Running Tests ------------- @@ -67,9 +70,11 @@ make sure you have satisfied all the prerequisites above. After you have done that, consider the following: - When building OpenMC, make sure you run CMake with - ``-DCMAKE_BUILD_TYPE=Debug``. Building with a release build will result in - some test failures due to differences in which compiler optimizations are - used. + ``-DOPENMC_ENABLE_STRICT_FP=on``. This prevents the compiler from applying + floating-point optimizations (such as replacing math library calls with + builtins or contracting multiply-add into FMA instructions) that can produce + bit-level differences across platforms and optimization levels. Any + ``CMAKE_BUILD_TYPE`` can be used. - Because tallies involve the sum of many floating point numbers, the non-associativity of floating point numbers can result in different answers especially when the number of threads is high (different order of operations). diff --git a/docs/source/usersguide/install.rst b/docs/source/usersguide/install.rst index 64d4801799..1f9a07b80c 100644 --- a/docs/source/usersguide/install.rst +++ b/docs/source/usersguide/install.rst @@ -383,6 +383,20 @@ OPENMC_USE_MPI options, please see the `FindMPI.cmake documentation `_. +.. _cmake_strict_fp: + +OPENMC_ENABLE_STRICT_FP + Disables compiler optimizations that change floating-point results relative to + unoptimized builds, improving cross-platform and cross-optimization-level + reproducibility. This disables FMA contraction (``-ffp-contract=off``) and + compiler builtin replacements of math functions like ``pow``, ``exp``, ``log`` + (``-fno-builtin``). It also keeps C/C++ assertions active by removing the + ``-DNDEBUG`` flag from ``RelWithDebInfo`` builds. Without this flag, these + optimizations can produce bit-level differences across platforms, compilers, + and optimization levels. This option should be used when running the test + suite. By default (off), the compiler is free to use all optimizations for + best performance. (Default: off) + OPENMC_FORCE_VENDORED_LIBS Forces OpenMC to use the submodules located in the vendor directory, as opposed to searching the system for already installed versions of those @@ -415,7 +429,10 @@ Release RelWithDebInfo (Default if no type is specified.) Enable optimization and debug. On most - platforms/compilers, this is equivalent to `-O2 -g`. + platforms/compilers, this is equivalent to `-O2 -g`. When + :ref:`OPENMC_ENABLE_STRICT_FP ` is enabled, OpenMC removes the + ``-DNDEBUG`` flag that CMake normally adds for this build type, so that + C/C++ assertions remain active. Example of configuring for Debug mode: diff --git a/include/openmc/output.h b/include/openmc/output.h index 940ea78ceb..7fcaa81ba4 100644 --- a/include/openmc/output.h +++ b/include/openmc/output.h @@ -10,6 +10,8 @@ namespace openmc { +extern "C" const bool STRICT_FP_ENABLED; + //! \brief Display the main title banner as well as information about the //! program developers, version, and date/time which the problem was run. void title(); diff --git a/openmc/lib/__init__.py b/openmc/lib/__init__.py index d2a794eb15..9b135370fe 100644 --- a/openmc/lib/__init__.py +++ b/openmc/lib/__init__.py @@ -49,6 +49,9 @@ def _libmesh_enabled(): def _uwuw_enabled(): return c_bool.in_dll(_dll, "UWUW_ENABLED").value +def _strict_fp_enabled(): + return c_bool.in_dll(_dll, "STRICT_FP_ENABLED").value + from .error import * from .core import * diff --git a/src/output.cpp b/src/output.cpp index 8f9802bb9b..e10e472661 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -44,6 +44,12 @@ namespace openmc { +#ifdef OPENMC_ENABLE_STRICT_FP +const bool STRICT_FP_ENABLED = true; +#else +const bool STRICT_FP_ENABLED = false; +#endif + //============================================================================== void title() @@ -317,6 +323,7 @@ void print_build_info() std::string coverage(n); std::string mcpl(n); std::string uwuw(n); + std::string strict_fp(n); #ifdef PHDF5 phdf5 = y; @@ -345,6 +352,9 @@ void print_build_info() #ifdef OPENMC_UWUW_ENABLED uwuw = y; #endif +#ifdef OPENMC_ENABLE_STRICT_FP + strict_fp = y; +#endif // Wraps macro variables in quotes #define STRINGIFY(x) STRINGIFY2(x) @@ -363,6 +373,7 @@ void print_build_info() fmt::print("Coverage testing: {}\n", coverage); fmt::print("Profiling flags: {}\n", profiling); fmt::print("UWUW support: {}\n", uwuw); + fmt::print("Strict FP: {}\n", strict_fp); } } diff --git a/tests/conftest.py b/tests/conftest.py index 71dd5ebf5d..8dda9f7565 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,9 +1,51 @@ import os +import hashlib import pytest import openmc +import openmc.lib from tests.regression_tests import config as regression_config +# MD5 hash of the official NNDC HDF5 cross_sections.xml file. +# Generated via: md5sum /path/to/nndc_hdf5/cross_sections.xml +_NNDC_XS_MD5 = "2d00773012eda670bc9f95d96a31c989" + +# Collected during pytest_configure, displayed at start and end of session +_environment_warnings = [] + + +def _check_build_environment(): + """Check STRICT_FP and cross section data, collecting any warnings.""" + if not openmc.lib._strict_fp_enabled(): + _environment_warnings.append( + "OpenMC was NOT built with -DOPENMC_ENABLE_STRICT_FP=on. " + "Regression test results may not match reference values due to " + "compiler floating-point optimizations. Rebuild with " + "-DOPENMC_ENABLE_STRICT_FP=on for reproducible results." + ) + + xs_path = os.environ.get("OPENMC_CROSS_SECTIONS") + if not xs_path: + _environment_warnings.append( + "OPENMC_CROSS_SECTIONS environment variable is not set. " + "Regression tests require the NNDC HDF5 cross section data." + ) + elif not os.path.isfile(xs_path): + _environment_warnings.append( + f"OPENMC_CROSS_SECTIONS ({xs_path}) is not a valid file path. " + "Regression tests require the NNDC HDF5 cross section data." + ) + else: + with open(xs_path, "rb") as f: + md5 = hashlib.md5(f.read()).hexdigest() + if md5 != _NNDC_XS_MD5: + _environment_warnings.append( + f"OPENMC_CROSS_SECTIONS ({xs_path}) does not match the " + "official NNDC HDF5 dataset. Regression tests expect the " + "NNDC data; results may differ with other cross section " + "libraries." + ) + def pytest_addoption(parser): parser.addoption('--exe') @@ -21,6 +63,28 @@ def pytest_configure(config): if config.getoption(opt) is not None: regression_config[opt] = config.getoption(opt) + _check_build_environment() + + +def _print_environment_warnings(terminalreporter): + """Print environment warnings as a visible section.""" + if _environment_warnings: + terminalreporter.section("OpenMC Environment Warnings") + for msg in _environment_warnings: + terminalreporter.line(f"WARNING: {msg}", yellow=True) + terminalreporter.line("") + + +def pytest_sessionstart(session): + """Print environment warnings at the start of the test session.""" + _print_environment_warnings(session.config.pluginmanager.get_plugin( + "terminalreporter")) + + +def pytest_terminal_summary(terminalreporter, exitstatus, config): + """Reprint environment warnings at the end so they aren't missed.""" + _print_environment_warnings(terminalreporter) + @pytest.fixture def run_in_tmpdir(tmpdir): diff --git a/tools/ci/gha-install.py b/tools/ci/gha-install.py index 1cc792f8d7..5488b95476 100644 --- a/tools/ci/gha-install.py +++ b/tools/ci/gha-install.py @@ -9,8 +9,8 @@ def install(omp=False, mpi=False, phdf5=False, dagmc=False, libmesh=False): os.mkdir('build') os.chdir('build') - # Build in debug mode by default with support for MCPL - cmake_cmd = ['cmake', '-DCMAKE_BUILD_TYPE=Debug', '-DOPENMC_USE_MCPL=on'] + # Build in RelWithDebInfo mode by default with support for MCPL + cmake_cmd = ['cmake', '-DCMAKE_BUILD_TYPE=RelWithDebInfo', '-DOPENMC_USE_MCPL=on'] # Turn off OpenMP if specified if not omp: @@ -43,6 +43,9 @@ def install(omp=False, mpi=False, phdf5=False, dagmc=False, libmesh=False): # Build in coverage mode for coverage testing cmake_cmd.append('-DOPENMC_ENABLE_COVERAGE=on') + # Enable strict FP for cross-platform reproducibility in CI + cmake_cmd.append('-DOPENMC_ENABLE_STRICT_FP=on') + # Build and install cmake_cmd.append('..') print(' '.join(cmake_cmd)) From 8081815b99bc1f13c7ea35da4bb11d82c6db4edd Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Thu, 26 Feb 2026 03:19:02 +0100 Subject: [PATCH 18/38] Add RegularMesh.get_indices_at_coords method (#3824) Co-authored-by: Paul Romano --- openmc/mesh.py | 59 +++++++++++++++++++++++++++- tests/unit_tests/test_mesh.py | 73 +++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 2 deletions(-) diff --git a/openmc/mesh.py b/openmc/mesh.py index 3f0802d6be..030a57218e 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -547,6 +547,10 @@ class StructuredMesh(MeshBase): def _grids(self): pass + @abstractmethod + def get_indices_at_coords(self, coords: Sequence[float]) -> tuple: + pass + @property def vertices(self): """Return coordinates of mesh vertices in Cartesian coordinates. Also @@ -1432,6 +1436,47 @@ class RegularMesh(StructuredMesh): return root_cell, cells + def get_indices_at_coords(self, coords: Sequence[float]) -> tuple: + """Finds the index of the mesh element at the specified coordinates. + + .. versionadded:: 0.15.4 + + Parameters + ---------- + coords : Sequence[float] + Cartesian coordinates of the point. + + Returns + ------- + tuple + Mesh indices matching the dimensionality of the mesh + + """ + ndim = self.n_dimension + if len(coords) < ndim: + raise ValueError( + f"coords must have at least {ndim} values for a " + f"{ndim}D mesh, got {len(coords)}" + ) + + coords_array = np.array(coords[:ndim]) + lower_left = np.array(self.lower_left) + upper_right = np.array(self.upper_right) + dimension = np.array(self.dimension) + + if np.any(coords_array < lower_left) or np.any(coords_array > upper_right): + raise ValueError( + f"coords {tuple(coords_array)} are outside mesh bounds " + f"[{tuple(lower_left)}, {tuple(upper_right)}]" + ) + + # Calculate spacing for each dimension + spacing = (upper_right - lower_left) / dimension + + # Calculate indices for each coordinate + indices = np.floor((coords_array - lower_left) / spacing).astype(int) + return tuple(int(i) for i in indices[:ndim]) + def Mesh(*args, **kwargs): warnings.warn("Mesh has been renamed RegularMesh. Future versions of " @@ -1643,6 +1688,11 @@ class RectilinearMesh(StructuredMesh): return element + def get_indices_at_coords(self, coords: Sequence[float]) -> tuple: + raise NotImplementedError( + "get_indices_at_coords is not yet implemented for RectilinearMesh" + ) + class CylindricalMesh(StructuredMesh): """A 3D cylindrical mesh @@ -1835,14 +1885,14 @@ class CylindricalMesh(StructuredMesh): self, coords: Sequence[float] ) -> tuple[int, int, int]: - """Finds the index of the mesh voxel at the specified x,y,z coordinates. + """Finds the index of the mesh element at the specified coordinates. .. versionadded:: 0.15.0 Parameters ---------- coords : Sequence[float] - The x, y, z axis coordinates + Cartesian coordinates of the point. Returns ------- @@ -2478,6 +2528,11 @@ class SphericalMesh(StructuredMesh): arr[..., 2] = z + origin[2] return arr + def get_indices_at_coords(self, coords: Sequence[float]) -> tuple: + raise NotImplementedError( + "get_indices_at_coords is not yet implemented for SphericalMesh" + ) + def require_statepoint_data(func): @wraps(func) diff --git a/tests/unit_tests/test_mesh.py b/tests/unit_tests/test_mesh.py index c5855a7b05..9d07eda0d9 100644 --- a/tests/unit_tests/test_mesh.py +++ b/tests/unit_tests/test_mesh.py @@ -920,3 +920,76 @@ def test_filter_time_mesh(run_in_tmpdir): f"Collision vs tracklength tallies disagree: chi2={chi2_stat:.2f} " f">= {crit=:.2f} ({dof=}, {alpha=})" ) + + +def test_regular_mesh_get_indices_at_coords(): + """Test get_indices_at_coords method for RegularMesh""" + # Create a 10x10x10 mesh from (0,0,0) to (1,1,1) + # Each voxel is 0.1 x 0.1 x 0.1 + mesh = openmc.RegularMesh() + mesh.lower_left = (0, 0, 0) + mesh.upper_right = (1, 1, 1) + mesh.dimension = [10, 10, 10] + + # Test lower-left corner maps to first voxel (0, 0, 0) + assert mesh.get_indices_at_coords([0.0, 0.0, 0.0]) == (0, 0, 0) + + # Test centroid of first voxel + # Voxel 0 spans [0.0, 0.1], so centroid is at 0.05 + assert mesh.get_indices_at_coords([0.05, 0.05, 0.05]) == (0, 0, 0) + + # Test centroid of last voxel maps correctly + # Voxel 9 spans [0.9, 1.0], so centroid is at 0.95 + assert mesh.get_indices_at_coords([0.95, 0.95, 0.95]) == (9, 9, 9) + + # Test a middle voxel + # Voxel 4 spans [0.4, 0.5], so 0.45 should map to it + assert mesh.get_indices_at_coords([0.45, 0.45, 0.45]) == (4, 4, 4) + + # Test mixed indices + assert mesh.get_indices_at_coords([0.05, 0.45, 0.95]) == (0, 4, 9) + assert mesh.get_indices_at_coords([0.95, 0.05, 0.45]) == (9, 0, 4) + + # Test coordinates outside mesh bounds raise ValueError + with pytest.raises(ValueError): + mesh.get_indices_at_coords([-0.5, 0.5, 0.5]) + with pytest.raises(ValueError): + mesh.get_indices_at_coords([1.5, 0.5, 0.5]) + with pytest.raises(ValueError): + mesh.get_indices_at_coords([0.5, -0.5, 0.5]) + with pytest.raises(ValueError): + mesh.get_indices_at_coords([0.5, 1.5, 0.5]) + with pytest.raises(ValueError): + mesh.get_indices_at_coords([0.5, 0.5, -0.5]) + with pytest.raises(ValueError): + mesh.get_indices_at_coords([0.5, 0.5, 1.5]) + + # Test that results match expected dimensionality (3D mesh returns 3-tuple) + result = mesh.get_indices_at_coords([0.5, 0.5, 0.5]) + assert isinstance(result, tuple) + assert len(result) == 3 + + # Test that indices can be used directly with centroids array + idx = mesh.get_indices_at_coords([0.95, 0.95, 0.95]) + centroid = mesh.centroids[idx] + np.testing.assert_array_almost_equal(centroid, [0.95, 0.95, 0.95]) + + # Test with a 2D mesh + mesh_2d = openmc.RegularMesh() + mesh_2d.lower_left = (0, 0) + mesh_2d.upper_right = (1, 1) + mesh_2d.dimension = [10, 10] + result_2d = mesh_2d.get_indices_at_coords([0.5, 0.5, 999.0]) + assert isinstance(result_2d, tuple) + assert len(result_2d) == 2 + assert result_2d == (5, 5) + + # Test with a 1D mesh + mesh_1d = openmc.RegularMesh() + mesh_1d.lower_left = [0] + mesh_1d.upper_right = [1] + mesh_1d.dimension = [10] + result_1d = mesh_1d.get_indices_at_coords([0.5, 999.0, 999.0]) + assert isinstance(result_1d, tuple) + assert len(result_1d) == 1 + assert result_1d == (5,) From 3ba8a9f078896ebab9db184d4e2b63ea127f24c0 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Thu, 26 Feb 2026 04:44:17 +0200 Subject: [PATCH 19/38] Correctly score pulse height tally when no cell filter is present (#3821) Co-authored-by: Paul Romano --- include/openmc/tallies/tally.h | 2 +- src/tallies/tally.cpp | 30 ++++++-------- src/tallies/tally_scoring.cpp | 75 +++++++++++++++------------------- 3 files changed, 48 insertions(+), 59 deletions(-) diff --git a/include/openmc/tallies/tally.h b/include/openmc/tallies/tally.h index 6161f823be..66e6ff764f 100644 --- a/include/openmc/tallies/tally.h +++ b/include/openmc/tallies/tally.h @@ -212,7 +212,7 @@ extern vector active_collision_tallies; extern vector active_meshsurf_tallies; extern vector active_surface_tallies; extern vector active_pulse_height_tallies; -extern vector pulse_height_cells; +extern vector pulse_height_cells; extern vector time_grid; } // namespace model diff --git a/src/tallies/tally.cpp b/src/tallies/tally.cpp index 32566721f8..13225adcaf 100644 --- a/src/tallies/tally.cpp +++ b/src/tallies/tally.cpp @@ -2,6 +2,7 @@ #include "openmc/array.h" #include "openmc/capi.h" +#include "openmc/cell.h" #include "openmc/constants.h" #include "openmc/container_util.h" #include "openmc/error.h" @@ -62,7 +63,7 @@ vector active_collision_tallies; vector active_meshsurf_tallies; vector active_surface_tallies; vector active_pulse_height_tallies; -vector pulse_height_cells; +vector pulse_height_cells; vector time_grid; } // namespace model @@ -632,29 +633,24 @@ void Tally::set_scores(const vector& scores) estimator_ = TallyEstimator::COLLISION; break; - case SCORE_PULSE_HEIGHT: + case SCORE_PULSE_HEIGHT: { if (non_cell_energy_present) { fatal_error("Pulse-height tallies are not compatible with filters " "other than CellFilter and EnergyFilter"); } type_ = TallyType::PULSE_HEIGHT; - - // Collecting indices of all cells covered by the filters in the pulse - // height tally in global variable pulse_height_cells - for (const auto& i_filt : filters_) { - auto cell_filter = - dynamic_cast(model::tally_filters[i_filt].get()); - if (cell_filter) { - const auto& cells = cell_filter->cells(); - for (int i = 0; i < cell_filter->n_bins(); i++) { - int cell_index = cells[i]; - if (!contains(model::pulse_height_cells, cell_index)) { - model::pulse_height_cells.push_back(cell_index); - } - } - } + // Collect all unique cell indices covered by this tally. + // If no CellFilter is present, all cells in the geometry are scored. + const auto* cell_filter_ptr = get_filter(); + int n = cell_filter_ptr ? cell_filter_ptr->n_bins() + : static_cast(model::cells.size()); + for (int i = 0; i < n; ++i) { + int32_t cell_index = cell_filter_ptr ? cell_filter_ptr->cells()[i] : i; + if (!contains(model::pulse_height_cells, cell_index)) + model::pulse_height_cells.push_back(cell_index); } break; + } case SCORE_IFP_TIME_NUM: case SCORE_IFP_BETA_NUM: diff --git a/src/tallies/tally_scoring.cpp b/src/tallies/tally_scoring.cpp index fb851f7838..cc7b16f44d 100644 --- a/src/tallies/tally_scoring.cpp +++ b/src/tallies/tally_scoring.cpp @@ -2672,56 +2672,49 @@ void score_pulse_height_tally(Particle& p, const vector& tallies) for (auto i_tally : tallies) { auto& tally {*model::tallies[i_tally]}; - // Determine all CellFilter in the tally - for (const auto& filter : tally.filters()) { - auto cell_filter = - dynamic_cast(model::tally_filters[filter].get()); - if (cell_filter != nullptr) { + // Find CellFilter in the tally (if any) to determine cells to loop over + const auto* cell_filter = tally.get_filter(); + const auto& cells = + cell_filter ? cell_filter->cells() : model::pulse_height_cells; - const auto& cells = cell_filter->cells(); - // Loop over all cells in the CellFilter - for (auto cell_index = 0; cell_index < cells.size(); ++cell_index) { - int cell_id = cells[cell_index]; + for (auto cell_id : cells) { + // Temporarily change cell of particle + p.n_coord() = 1; + p.coord(0).cell() = cell_id; - // Temporarily change cell of particle - p.n_coord() = 1; - p.coord(0).cell() = cell_id; + // Determine index of cell in model::pulse_height_cells + auto it = std::find(model::pulse_height_cells.begin(), + model::pulse_height_cells.end(), cell_id); + int index = std::distance(model::pulse_height_cells.begin(), it); - // Determine index of cell in model::pulse_height_cells - auto it = std::find(model::pulse_height_cells.begin(), - model::pulse_height_cells.end(), cell_id); - int index = std::distance(model::pulse_height_cells.begin(), it); + // Temporarily change energy of particle to pulse-height value + p.E_last() = p.pht_storage()[index]; - // Temporarily change energy of particle to pulse-height value - p.E_last() = p.pht_storage()[index]; + // Initialize an iterator over valid filter bin combinations. If + // there are no valid combinations, use a continue statement to ensure + // we skip the assume_separate break below. + auto filter_iter = FilterBinIter(tally, p); + auto end = FilterBinIter(tally, true, &p.filter_matches()); + if (filter_iter == end) + continue; - // Initialize an iterator over valid filter bin combinations. If - // there are no valid combinations, use a continue statement to ensure - // we skip the assume_separate break below. - auto filter_iter = FilterBinIter(tally, p); - auto end = FilterBinIter(tally, true, &p.filter_matches()); - if (filter_iter == end) - continue; + // Loop over filter bins. + for (; filter_iter != end; ++filter_iter) { + auto filter_index = filter_iter.index_; + auto filter_weight = filter_iter.weight_; - // Loop over filter bins. - for (; filter_iter != end; ++filter_iter) { - auto filter_index = filter_iter.index_; - auto filter_weight = filter_iter.weight_; - - // Loop over scores. - for (auto score_index = 0; score_index < tally.scores_.size(); - ++score_index) { + // Loop over scores. + for (auto score_index = 0; score_index < tally.scores_.size(); + ++score_index) { #pragma omp atomic - tally.results_(filter_index, score_index, TallyResult::VALUE) += - filter_weight; - } - } - - // Reset all the filter matches for the next tally event. - for (auto& match : p.filter_matches()) - match.bins_present_ = false; + tally.results_(filter_index, score_index, TallyResult::VALUE) += + filter_weight; } } + + // Reset all the filter matches for the next tally event. + for (auto& match : p.filter_matches()) + match.bins_present_ = false; } // Restore cell/energy p.n_coord() = orig_n_coord; From 6050c789cab4e074c2c4bf9e1120d3ce97ebe4fc Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Thu, 26 Feb 2026 04:14:44 +0100 Subject: [PATCH 20/38] making use of endf.get_evaluations (#3819) Co-authored-by: Paul Romano --- openmc/data/endf.py | 28 +--------------------------- openmc/data/neutron.py | 4 +--- 2 files changed, 2 insertions(+), 30 deletions(-) diff --git a/openmc/data/endf.py b/openmc/data/endf.py index e163ad4c22..c50431dc7f 100644 --- a/openmc/data/endf.py +++ b/openmc/data/endf.py @@ -12,7 +12,7 @@ import re from .data import gnds_name from .function import Tabulated1D -from endf.material import _LIBRARY, _SUBLIBRARY +from endf.material import _LIBRARY, _SUBLIBRARY, get_materials as get_evaluations from endf.incident_neutron import SUM_RULES from endf.records import ( float_endf, @@ -39,32 +39,6 @@ def get_tab1_record(file_obj): return params, Tabulated1D(tab.x, tab.y, tab.breakpoints, tab.interpolation) -def get_evaluations(filename): - """Return a list of all evaluations within an ENDF file. - - Parameters - ---------- - filename : str - Path to ENDF-6 formatted file - - Returns - ------- - list - A list of :class:`openmc.data.endf.Evaluation` instances. - - """ - evaluations = [] - with open(str(filename), 'r') as fh: - while True: - pos = fh.tell() - line = fh.readline() - if line[66:70] == ' -1': - break - fh.seek(pos) - evaluations.append(Evaluation(fh)) - return evaluations - - class Evaluation: """ENDF material evaluation with multiple files/sections diff --git a/openmc/data/neutron.py b/openmc/data/neutron.py index 71927cbed6..492cdd7f3b 100644 --- a/openmc/data/neutron.py +++ b/openmc/data/neutron.py @@ -805,9 +805,7 @@ class IncidentNeutron(EqualityMixin): # Helper function to get a cross section from an ENDF file on a # given energy grid def get_file3_xs(ev, mt, E): - file_obj = StringIO(ev.section[3, mt]) - get_head_record(file_obj) - _, xs = get_tab1_record(file_obj) + xs = ev.section_data[3, mt]['sigma'] return xs(E) heating_local = Reaction(901) From 322b741fdebe7f2faaeb62e721e1f5de74611039 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Thu, 26 Feb 2026 09:30:08 +0200 Subject: [PATCH 21/38] Support Mixture distributions in combine_distributions (#3784) Co-authored-by: Paul Romano --- openmc/stats/univariate.py | 31 ++++++++++++++++++++++++------- tests/unit_tests/test_stats.py | 17 +++++++++++++++++ 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/openmc/stats/univariate.py b/openmc/stats/univariate.py index c6ce217fb2..a70737a347 100644 --- a/openmc/stats/univariate.py +++ b/openmc/stats/univariate.py @@ -2065,31 +2065,48 @@ class Mixture(Univariate): def combine_distributions( - dists: Sequence[Discrete | Tabular], + dists: Sequence[Discrete | Tabular | Mixture], probs: Sequence[float] ): """Combine distributions with specified probabilities This function can be used to combine multiple instances of - :class:`~openmc.stats.Discrete` and `~openmc.stats.Tabular`. Multiple - discrete distributions are merged into a single distribution and the - remainder of the distributions are put into a :class:`~openmc.stats.Mixture` - distribution. + :class:`~openmc.stats.Discrete`, :class:`~openmc.stats.Tabular` and + :class:`~openmc.stats.Mixture` of them. Multiple discrete distributions are + merged into a single distribution and the remainder of the distributions are + put into a :class:`~openmc.stats.Mixture` distribution. .. versionadded:: 0.13.1 Parameters ---------- - dists : sequence of openmc.stats.Discrete or openmc.stats.Tabular + dists : sequence of openmc.stats.Discrete, openmc.stats.Tabular, or openmc.stats.Mixture Distributions to combine probs : sequence of float Probability (or intensity) of each distribution """ + new_probs = [] + new_dists = [] for i, dist in enumerate(dists): - cv.check_type(f'dists[{i}]', dist, (Discrete, Tabular)) + cv.check_type(f'dists[{i}]', dist, (Discrete, Tabular, Mixture)) cv.check_type(f'probs[{i}]', probs[i], Real) cv.check_greater_than(f'probs[{i}]', probs[i], 0.0) + if isinstance(dist, Mixture): + if dist.bias is not None: + warn("A Mixture distribution with a bias specified was passed " + "to combine_distributions. The bias will be discarded " + "during flattening.") + for j, d in enumerate(dist.distribution): + cv.check_type(f'dists[{i}].distribution[{j}]', d, (Discrete, Tabular)) + new_probs.append(probs[i]*dist.probability[j]) + new_dists.append(d) + else: + new_probs.append(probs[i]) + new_dists.append(dist) + + probs = new_probs + dists = new_dists # Get list of discrete/continuous distribution indices discrete_index = [i for i, d in enumerate(dists) if isinstance(d, Discrete)] diff --git a/tests/unit_tests/test_stats.py b/tests/unit_tests/test_stats.py index 71679024fd..669d5b74c2 100644 --- a/tests/unit_tests/test_stats.py +++ b/tests/unit_tests/test_stats.py @@ -835,6 +835,23 @@ def test_combine_distributions(): assert isinstance(mixed, openmc.stats.Mixture) assert len(mixed.distribution) == 2 assert len(mixed.probability) == 2 + assert mixed == openmc.stats.combine_distributions([mixed], [1.0]) + + # Mixture combined with another distribution: probabilities should be + # correctly scaled when the Mixture is flattened + d_a = openmc.stats.delta_function(1.0) + d_b = openmc.stats.delta_function(2.0) + m = openmc.stats.Mixture([0.3, 0.7], [d_a, d_b]) + extra = openmc.stats.delta_function(3.0) + result = openmc.stats.combine_distributions([m, extra], [0.5, 0.5]) + assert isinstance(result, openmc.stats.Discrete) + assert result.x == pytest.approx([1.0, 2.0, 3.0]) + assert result.p == pytest.approx([0.5*0.3, 0.5*0.7, 0.5]) + + # Passing a Mixture with a bias should warn that the bias is dropped + biased_m = openmc.stats.Mixture([0.5, 0.5], [d_a, d_b], bias=[0.8, 0.2]) + with pytest.warns(UserWarning, match='bias'): + openmc.stats.combine_distributions([biased_m], [1.0]) # Single tabular returns a tabular distribution with scaled probabilities t_single = openmc.stats.Tabular([0.0, 1.0], [2.0, 0.0]) From 17d4164242f3e7692654827e410d8293901c9285 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 26 Feb 2026 02:14:28 -0600 Subject: [PATCH 22/38] Update copyright to 2026 (#3834) --- LICENSE | 2 +- docs/source/conf.py | 2 +- docs/source/license.rst | 2 +- man/man1/openmc.1 | 2 +- src/output.cpp | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/LICENSE b/LICENSE index 8a60b66bf9..b5dfd1748e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2011-2025 Massachusetts Institute of Technology, UChicago Argonne +Copyright (c) 2011-2026 Massachusetts Institute of Technology, UChicago Argonne LLC, and OpenMC contributors Permission is hereby granted, free of charge, to any person obtaining a copy of diff --git a/docs/source/conf.py b/docs/source/conf.py index 826c20022a..3a2e7fb93b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -62,7 +62,7 @@ master_doc = 'index' # General information about the project. project = 'OpenMC' -copyright = '2011-2025, Massachusetts Institute of Technology, UChicago Argonne LLC, and OpenMC contributors' +copyright = '2011-2026, Massachusetts Institute of Technology, UChicago Argonne LLC, and OpenMC contributors' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/docs/source/license.rst b/docs/source/license.rst index 0a90a74413..1ec9fc04a6 100644 --- a/docs/source/license.rst +++ b/docs/source/license.rst @@ -4,7 +4,7 @@ License Agreement ================= -Copyright © 2011-2025 Massachusetts Institute of Technology, UChicago Argonne +Copyright © 2011-2026 Massachusetts Institute of Technology, UChicago Argonne LLC, and OpenMC contributors Permission is hereby granted, free of charge, to any person obtaining a copy of diff --git a/man/man1/openmc.1 b/man/man1/openmc.1 index 30e8b2ce47..3205b4340c 100644 --- a/man/man1/openmc.1 +++ b/man/man1/openmc.1 @@ -66,7 +66,7 @@ Indicates the default path to an HDF5 file that contains multi-group cross section libraries if the user has not specified the tag in .I materials.xml\fP. .SH LICENSE -Copyright \(co 2011-2025 Massachusetts Institute of Technology, UChicago +Copyright \(co 2011-2026 Massachusetts Institute of Technology, UChicago Argonne LLC, and OpenMC contributors. .PP Permission is hereby granted, free of charge, to any person obtaining a copy of diff --git a/src/output.cpp b/src/output.cpp index e10e472661..c66c313dce 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -81,7 +81,7 @@ void title() // Write version information fmt::print( " | The OpenMC Monte Carlo Code\n" - " Copyright | 2011-2025 MIT, UChicago Argonne LLC, and contributors\n" + " Copyright | 2011-2026 MIT, UChicago Argonne LLC, and contributors\n" " License | https://docs.openmc.org/en/latest/license.html\n" " Version | {}.{}.{}{}{}\n", VERSION_MAJOR, VERSION_MINOR, VERSION_RELEASE, VERSION_DEV ? "-dev" : "", @@ -301,7 +301,7 @@ void print_version() fmt::print("OpenMC version {}.{}.{}{}{}\n", VERSION_MAJOR, VERSION_MINOR, VERSION_RELEASE, VERSION_DEV ? "-dev" : "", VERSION_COMMIT_COUNT); fmt::print("Commit hash: {}\n", VERSION_COMMIT_HASH); - fmt::print("Copyright (c) 2011-2025 MIT, UChicago Argonne LLC, and " + fmt::print("Copyright (c) 2011-2026 MIT, UChicago Argonne LLC, and " "contributors\nMIT/X license at " "\n"); } From 3b5ac08bfb754ef93ff3db12fcda2ecbd0dbe464 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 26 Feb 2026 11:26:39 -0600 Subject: [PATCH 23/38] Skip DAGMC lost particles test (#3836) --- tests/unit_tests/dagmc/test_lost_particles.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit_tests/dagmc/test_lost_particles.py b/tests/unit_tests/dagmc/test_lost_particles.py index 931a37f64f..48b6cf1657 100644 --- a/tests/unit_tests/dagmc/test_lost_particles.py +++ b/tests/unit_tests/dagmc/test_lost_particles.py @@ -66,6 +66,7 @@ def broken_dagmc_model(request): return model +@pytest.mark.skip(reason="Test causes CI to hang intermittently") def test_lost_particles(run_in_tmpdir, broken_dagmc_model): broken_dagmc_model.export_to_xml() # ensure that particles will be lost when cell intersections can't be found From 1d9a8f542b2b4fc588f0961c59ea674e4c6ca5ca Mon Sep 17 00:00:00 2001 From: Micah Gale Date: Thu, 26 Feb 2026 18:18:04 -0600 Subject: [PATCH 24/38] Add Python 3.14 to testing matrix and drop Python 3.11 (#3642) Co-authored-by: GuySten Co-authored-by: Paul Romano --- .github/workflows/ci.yml | 19 +++++++++++-------- pyproject.toml | 4 ++-- tools/ci/gha-install.sh | 2 +- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d0382d8d6d..3e232dce0f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,7 @@ jobs: runs-on: ubuntu-22.04 strategy: matrix: - python-version: ["3.11"] + python-version: ["3.12"] mpi: [n, y] omp: [n, y] dagmc: [n] @@ -51,26 +51,29 @@ jobs: event: [n] include: - - python-version: "3.12" - omp: n - mpi: n - python-version: "3.13" omp: n mpi: n + - python-version: "3.14" + omp: n + mpi: n + - python-version: "3.14t" + omp: n + mpi: n - dagmc: y - python-version: "3.11" + python-version: "3.12" mpi: y omp: y - libmesh: y - python-version: "3.11" + python-version: "3.12" mpi: y omp: y - libmesh: y - python-version: "3.11" + python-version: "3.12" mpi: n omp: y - event: y - python-version: "3.11" + python-version: "3.12" omp: y mpi: n name: "Python ${{ matrix.python-version }} (omp=${{ matrix.omp }}, diff --git a/pyproject.toml b/pyproject.toml index 21342155a5..b2b5ff5e40 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ authors = [ ] description = "OpenMC" dynamic = ["version"] -requires-python = ">=3.11" +requires-python = ">=3.12" license = {file = "LICENSE"} classifiers = [ "Development Status :: 4 - Beta", @@ -21,9 +21,9 @@ classifiers = [ "Topic :: Scientific/Engineering", "Programming Language :: C++", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", ] dependencies = [ "numpy", diff --git a/tools/ci/gha-install.sh b/tools/ci/gha-install.sh index f405baedd5..4cf62afbb7 100755 --- a/tools/ci/gha-install.sh +++ b/tools/ci/gha-install.sh @@ -35,7 +35,7 @@ if [[ $MPI == 'y' ]]; then export HDF5_MPI=ON export HDF5_DIR=/usr/lib/x86_64-linux-gnu/hdf5/mpich # Install h5py without build isolation to pick up already installed mpi4py - pip install Cython pkgconfig + pip install setuptools Cython pkgconfig pip install --no-build-isolation --no-binary=h5py h5py fi From b3788f11e161171e842766a7b354afb4ad3f657a Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 27 Feb 2026 05:56:00 -0600 Subject: [PATCH 25/38] Use clang-format version 18 for CI format checks (#3840) --- .github/pull_request_template.md | 2 +- .github/workflows/format-check.yml | 2 +- AGENTS.md | 4 ++-- docs/source/devguide/styleguide.rst | 2 +- include/openmc/volume_calc.h | 2 +- src/mgxs.cpp | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 68a6e692a4..958cc21fdd 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -13,7 +13,7 @@ Fixes # (issue) # Checklist - [ ] I have performed a self-review of my own code -- [ ] I have run [clang-format](https://docs.openmc.org/en/latest/devguide/styleguide.html#automatic-formatting) (version 15) on any C++ source files (if applicable) +- [ ] I have run [clang-format](https://docs.openmc.org/en/latest/devguide/styleguide.html#automatic-formatting) (version 18) on any C++ source files (if applicable) - [ ] I have followed the [style guidelines](https://docs.openmc.org/en/latest/devguide/styleguide.html#python) for Python source files (if applicable) - [ ] I have made corresponding changes to the documentation (if applicable) - [ ] I have added tests that prove my fix is effective or that my feature works (if applicable) diff --git a/.github/workflows/format-check.yml b/.github/workflows/format-check.yml index 42b28b2cdc..ddb29be5a4 100644 --- a/.github/workflows/format-check.yml +++ b/.github/workflows/format-check.yml @@ -31,7 +31,7 @@ jobs: style: file files-changed-only: true tidy-checks: '-*' - version: '15' # clang-format version + version: '18' # clang-format version format-review: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'cpp-format-suggest') }} passive-reviews: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'cpp-format-suggest') }} file-annotations: true diff --git a/AGENTS.md b/AGENTS.md index d24eb68da6..0ff2abbdc4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -229,7 +229,7 @@ When modifying C++ public APIs, update corresponding ctypes signatures in `openm - **Include order**: Related header first, then C/C++ stdlib, third-party libs, local headers - **Comments**: C++-style (`//`) only, never C-style (`/* */`) - **Standard**: C++17 features allowed -- **Formatting**: Run `clang-format` (version 15) before committing; install via `tools/dev/install-commit-hooks.sh` +- **Formatting**: Run `clang-format` (version 18) before committing; install via `tools/dev/install-commit-hooks.sh` ### Python Style - **PEP8** compliant @@ -295,4 +295,4 @@ Check for optional features: 2. **ID conflicts**: Python objects with duplicate IDs trigger `IDWarning`, use `reset_auto_ids()` between tests 3. **MPI builds**: Code must work with and without MPI; use `#ifdef OPENMC_MPI` guards 4. **Path handling**: Use `pathlib.Path` in new Python code, not `os.path` -5. **Clang-format version**: CI uses version 15; other versions may produce different formatting +5. **Clang-format version**: CI uses version 18; other versions may produce different formatting diff --git a/docs/source/devguide/styleguide.rst b/docs/source/devguide/styleguide.rst index 2c882b0341..a5599566e2 100644 --- a/docs/source/devguide/styleguide.rst +++ b/docs/source/devguide/styleguide.rst @@ -30,7 +30,7 @@ whenever a file is saved. For example, `Visual Studio Code support for running clang-format. .. note:: - OpenMC's CI uses `clang-format` version 15. A different version of `clang-format` + OpenMC's CI uses `clang-format` version 18. A different version of `clang-format` may produce different line changes and as a result fail the CI test. Miscellaneous diff --git a/include/openmc/volume_calc.h b/include/openmc/volume_calc.h index 9d3f1d0261..ef75ec0653 100644 --- a/include/openmc/volume_calc.h +++ b/include/openmc/volume_calc.h @@ -34,7 +34,7 @@ public: vector atoms; //!< Number of atoms for each nuclide vector uncertainty; //!< Uncertainty on number of atoms int iterations; //!< Number of iterations needed to obtain the results - }; // Results for a single domain + }; // Results for a single domain // Constructors VolumeCalculation(pugi::xml_node node); diff --git a/src/mgxs.cpp b/src/mgxs.cpp index 65a7a0c55c..a0fe4060dd 100644 --- a/src/mgxs.cpp +++ b/src/mgxs.cpp @@ -365,7 +365,7 @@ Mgxs::Mgxs(const std::string& in_name, const vector& mat_kTs, } } } // end switch - } // end microscopic temperature loop + } // end microscopic temperature loop // Now combine the microscopic data at each relevant temperature // We will do this by treating the multiple temperatures of a nuclide as From 83a7b36add42f1cdbf71e2d0ca546cd96d4899c7 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Sun, 1 Mar 2026 13:52:51 -0600 Subject: [PATCH 26/38] Speed up Docker build (#3841) Co-authored-by: Jonathan Shimwell --- Dockerfile | 29 ++++++++--------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/Dockerfile b/Dockerfile index a163a28102..67cd37c591 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,11 +33,6 @@ ARG build_libmesh # Set default value of HOME to /root ENV HOME=/root -# Embree variables -ENV EMBREE_TAG='v4.3.1' -ENV EMBREE_REPO='https://github.com/embree/embree' -ENV EMBREE_INSTALL_DIR=$HOME/EMBREE/ - # MOAB variables ENV MOAB_TAG='5.5.1' ENV MOAB_REPO='https://bitbucket.org/fathomteam/moab/' @@ -61,7 +56,7 @@ ENV LIBMESH_INSTALL_DIR=$HOME/LIBMESH ENV NJOY_REPO='https://github.com/njoy/NJOY2016' # Setup environment variables for Docker image -ENV LD_LIBRARY_PATH=${DAGMC_INSTALL_DIR}/lib:$LD_LIBRARY_PATH \ +ENV LD_LIBRARY_PATH=${DAGMC_INSTALL_DIR}/lib:${LD_LIBRARY_PATH:-} \ OPENMC_ENDF_DATA=/root/endf-b-vii.1 \ DEBIAN_FRONTEND=noninteractive @@ -71,7 +66,7 @@ RUN apt-get update -y && \ apt-get install -y \ python3-pip python-is-python3 wget git build-essential cmake \ mpich libmpich-dev libhdf5-serial-dev libhdf5-mpich-dev \ - libpng-dev python3-venv && \ + libpng-dev libpugixml-dev libfmt-dev catch2 python3-venv && \ apt-get autoremove # create virtual enviroment to avoid externally managed environment error @@ -94,22 +89,12 @@ RUN cd $HOME \ RUN if [ "$build_dagmc" = "on" ]; then \ # Install addition packages required for DAGMC - apt-get -y install libeigen3-dev libnetcdf-dev libtbb-dev libglfw3-dev \ + apt-get -y install \ + libeigen3-dev libnetcdf-dev libtbb-dev libglfw3-dev libembree-dev \ && pip install --upgrade numpy \ && pip install --no-cache-dir setuptools cython \ - # Clone and install EMBREE - && mkdir -p $HOME/EMBREE && cd $HOME/EMBREE \ - && git clone --single-branch -b ${EMBREE_TAG} --depth 1 ${EMBREE_REPO} \ - && mkdir build && cd build \ - && cmake ../embree \ - -DCMAKE_INSTALL_PREFIX=${EMBREE_INSTALL_DIR} \ - -DEMBREE_MAX_ISA=NONE \ - -DEMBREE_ISA_SSE42=ON \ - -DEMBREE_ISPC_SUPPORT=OFF \ - && make 2>/dev/null -j${compile_cores} install \ - && rm -rf ${EMBREE_INSTALL_DIR}/build ${EMBREE_INSTALL_DIR}/embree ; \ # Clone and install MOAB - mkdir -p $HOME/MOAB && cd $HOME/MOAB \ + && mkdir -p $HOME/MOAB && cd $HOME/MOAB \ && git clone --single-branch -b ${MOAB_TAG} --depth 1 ${MOAB_REPO} \ && mkdir build && cd build \ && cmake ../moab -DCMAKE_BUILD_TYPE=Release \ @@ -118,6 +103,7 @@ RUN if [ "$build_dagmc" = "on" ]; then \ -DBUILD_SHARED_LIBS=OFF \ -DENABLE_FORTRAN=OFF \ -DENABLE_BLASLAPACK=OFF \ + -DENABLE_TESTING=OFF \ && make 2>/dev/null -j${compile_cores} install \ && cmake ../moab \ -DENABLE_PYMOAB=ON \ @@ -133,7 +119,7 @@ RUN if [ "$build_dagmc" = "on" ]; then \ && mkdir build && cd build \ && cmake ../double-down -DCMAKE_INSTALL_PREFIX=${DD_INSTALL_DIR} \ -DMOAB_DIR=/usr/local \ - -DEMBREE_DIR=${EMBREE_INSTALL_DIR} \ + -DEMBREE_DIR=/usr \ && make 2>/dev/null -j${compile_cores} install \ && rm -rf ${DD_INSTALL_DIR}/build ${DD_INSTALL_DIR}/double-down ; \ # Clone and install DAGMC @@ -147,6 +133,7 @@ RUN if [ "$build_dagmc" = "on" ]; then \ -DDOUBLE_DOWN_DIR=${DD_INSTALL_DIR} \ -DCMAKE_PREFIX_PATH=${DD_INSTALL_DIR}/lib \ -DBUILD_STATIC_LIBS=OFF \ + -DBUILD_TESTS=OFF \ && make 2>/dev/null -j${compile_cores} install \ && rm -rf ${DAGMC_INSTALL_DIR}/DAGMC ${DAGMC_INSTALL_DIR}/build ; \ fi From 823b4c96c9107c4264d89e2bdacf46bae61b778e Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Mon, 2 Mar 2026 14:53:20 +0200 Subject: [PATCH 27/38] Speed up depletion with transfer rates (#3839) --- openmc/deplete/chain.py | 55 +++++++++++++++++------------------------ 1 file changed, 23 insertions(+), 32 deletions(-) diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index a835face72..689546b86b 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -18,7 +18,7 @@ from typing import List import lxml.etree as ET -from openmc.checkvalue import check_type, check_greater_than, PathLike +from openmc.checkvalue import check_type, check_length, check_greater_than, PathLike from openmc.data import gnds_name, zam from openmc.exceptions import DataError from .nuclide import FissionYieldDistribution, Nuclide @@ -807,37 +807,29 @@ class Chain: # Use DOK as intermediate representation n = len(self) matrix = dok_array((n, n)) + + check_type("mats", mats, (tuple, str)) + if not isinstance(mats, str): + check_type("mats", mats, tuple, str) + check_length("mats", mats, 2, 2) + dest_mat, mat = mats + else: + mat = mats + dest_mat = None + + # Build transfer term + components = tr_rates.get_components(mat, current_timestep, dest_mat) for i, nuc in enumerate(self.nuclides): elm = re.split(r'\d+', nuc.name)[0] - # Build transfer terms (nuclide transfer only) - if isinstance(mats, str): - mat = mats - components = tr_rates.get_components(mat, current_timestep) - if not components: - break - if elm in components: - matrix[i, i] = sum( - tr_rates.get_external_rate(mat, elm, current_timestep)) - elif nuc.name in components: - matrix[i, i] = sum( - tr_rates.get_external_rate(mat, nuc.name, current_timestep)) - else: - matrix[i, i] = 0.0 - - # Build transfer terms (transfer from one material into another) - elif isinstance(mats, tuple): - dest_mat, mat = mats - components = tr_rates.get_components(mat, current_timestep, dest_mat) - if elm in components: - matrix[i, i] = tr_rates.get_external_rate( - mat, elm, current_timestep, dest_mat)[0] - elif nuc.name in components: - matrix[i, i] = tr_rates.get_external_rate( - mat, nuc.name, current_timestep, dest_mat)[0] - else: - matrix[i, i] = 0.0 - + if elm in components: + key = elm + elif nuc.name in components: + key = nuc.name + else: + continue + matrix[i, i] = sum(tr_rates.get_external_rate(mat, key, current_timestep, dest_mat)) + # Return CSC instead of DOK return matrix.tocsc() @@ -866,14 +858,13 @@ class Chain: # Use DOK as intermediate representation n = len(self) vector = dok_array((n, 1)) + components = ext_source_rates.get_components(mat, current_timestep) for i, nuc in enumerate(self.nuclides): # Build source term vector - if nuc.name in ext_source_rates.get_components(mat, current_timestep): + if nuc.name in components: vector[i] = sum(ext_source_rates.get_external_rate( mat, nuc.name, current_timestep)) - else: - vector[i] = 0.0 # Return CSC instead of DOK return vector.tocsc() From 49b896b0eb339d4cd6cbba549ee75feb7ee130f4 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 2 Mar 2026 21:06:52 -0600 Subject: [PATCH 28/38] Enable "hybrid" tallies in `get_microxs_and_flux` (#3831) --- openmc/deplete/microxs.py | 147 ++++++++++++++++-- tests/regression_tests/microxs/test.py | 12 +- .../test_reference_materials_hybrid.csv | 7 + tests/unit_tests/test_deplete_microxs.py | 129 ++++++++++++++- 4 files changed, 279 insertions(+), 16 deletions(-) create mode 100644 tests/regression_tests/microxs/test_reference_materials_hybrid.csv diff --git a/openmc/deplete/microxs.py b/openmc/deplete/microxs.py index 879a2d4ee9..e6e2dbce24 100644 --- a/openmc/deplete/microxs.py +++ b/openmc/deplete/microxs.py @@ -50,7 +50,8 @@ def get_microxs_and_flux( chain_file: PathLike | Chain | None = None, path_statepoint: PathLike | None = None, path_input: PathLike | None = None, - run_kwargs=None + run_kwargs=None, + reaction_rate_opts: dict | None = None, ) -> tuple[list[np.ndarray], list[MicroXS]]: """Generate microscopic cross sections and fluxes for multiple domains. @@ -80,10 +81,12 @@ def get_microxs_and_flux( Energy group boundaries in [eV] or the name of the group structure. If left as None energies will default to [0.0, 100e6] reaction_rate_mode : {"direct", "flux"}, optional - Indicate how reaction rates should be calculated. The "direct" method - tallies reaction rates directly. The "flux" method tallies a multigroup - flux spectrum and then collapses multigroup reaction rates after a - transport solve (with an option to tally some reaction rates directly). + The "direct" method tallies reaction rates directly (per energy + group). The "flux" method tallies a multigroup flux spectrum and then + collapses reaction rates after a transport solve. When + `reaction_rate_opts` is provided with `reaction_rate_mode='flux'`, the + specified nuclide/reaction pairs are tallied directly and those values + override the flux-collapsed values. chain_file : PathLike or Chain, optional Path to the depletion chain XML file or an instance of openmc.deplete.Chain. Used to determine cross sections for materials not @@ -99,6 +102,10 @@ def get_microxs_and_flux( not kept. run_kwargs : dict, optional Keyword arguments passed to :meth:`openmc.Model.run` + reaction_rate_opts : dict, optional + When `reaction_rate_mode="flux"`, allows selecting a subset of + nuclide/reaction pairs to be computed via direct reaction-rate tallies + (per energy group). Supported keys: "nuclides", "reactions". Returns ------- @@ -153,12 +160,36 @@ def get_microxs_and_flux( flux_tally.scores = ['flux'] model.tallies = [flux_tally] + # Prepare reaction-rate tally for 'direct' or subset for 'flux' with opts + rr_tally = None + rr_nuclides: list[str] = [] + rr_reactions: list[str] = [] if reaction_rate_mode == 'direct': + rr_nuclides = list(nuclides) + rr_reactions = list(reactions) + elif reaction_rate_mode == 'flux' and reaction_rate_opts: + opts = reaction_rate_opts or {} + rr_nuclides = list(opts.get('nuclides', [])) + rr_reactions = list(opts.get('reactions', [])) + # Keep only requested pairs within overall sets + if rr_nuclides: + rr_nuclides = [n for n in rr_nuclides if n in set(nuclides)] + if rr_reactions: + rr_reactions = [r for r in rr_reactions if r in set(reactions)] + + # Only construct tally if both lists are non-empty + if rr_nuclides and rr_reactions: rr_tally = openmc.Tally(name='MicroXS RR') - rr_tally.filters = [domain_filter, energy_filter] - rr_tally.nuclides = nuclides + # Use 1-group energy filter for RR in flux mode + if reaction_rate_mode == 'flux': + rr_energy_filter = openmc.EnergyFilter( + [energy_filter.values[0], energy_filter.values[-1]]) + else: + rr_energy_filter = energy_filter + rr_tally.filters = [domain_filter, rr_energy_filter] + rr_tally.nuclides = rr_nuclides rr_tally.multiply_density = False - rr_tally.scores = reactions + rr_tally.scores = rr_reactions model.tallies.append(rr_tally) if openmc.lib.is_initialized: @@ -196,7 +227,7 @@ def get_microxs_and_flux( # Read in tally results (on all ranks) with StatePoint(statepoint_path) as sp: - if reaction_rate_mode == 'direct': + if rr_tally is not None: rr_tally = sp.tallies[rr_tally.id] rr_tally._read_results() flux_tally = sp.tallies[flux_tally.id] @@ -209,24 +240,31 @@ def get_microxs_and_flux( # Create list where each item corresponds to one domain fluxes = list(flux.squeeze((1, 2))) - if reaction_rate_mode == 'direct': + # If we built a reaction-rate tally, compute microscopic cross sections + if rr_tally is not None: # Get reaction rates reaction_rates = rr_tally.get_reshaped_data() # (domains, groups, nuclides, reactions) # Make energy groups last dimension reaction_rates = np.moveaxis(reaction_rates, 1, -1) # (domains, nuclides, reactions, groups) + # If RR is 1-group, sum flux over groups + if reaction_rate_mode == "flux": + flux = flux.sum(axis=-1, keepdims=True) # (domains, 1, 1, 1) + # Divide RR by flux to get microscopic cross sections. The indexing # ensures that only non-zero flux values are used, and broadcasting is # applied to align the shapes of reaction_rates and flux for division. - xs = np.empty_like(reaction_rates) # (domains, nuclides, reactions, groups) + xs = np.zeros_like(reaction_rates) # (domains, nuclides, reactions, groups) d, _, _, g = np.nonzero(flux) xs[d, ..., g] = reaction_rates[d, ..., g] / flux[d, :, :, g] # Create lists where each item corresponds to one domain - micros = [MicroXS(xs_i, nuclides, reactions) for xs_i in xs] - else: - micros = [MicroXS.from_multigroup_flux( + direct_micros = [MicroXS(xs_i, rr_nuclides, rr_reactions) for xs_i in xs] + + # If using flux mode, compute flux-collapsed microscopic XS + if reaction_rate_mode == 'flux': + flux_micros = [MicroXS.from_multigroup_flux( energies=energies, multigroup_flux=flux_i, chain_file=chain_file, @@ -234,6 +272,14 @@ def get_microxs_and_flux( reactions=reactions ) for flux_i in fluxes] + # Decide which micros to use and merge if needed + if reaction_rate_mode == 'flux' and rr_tally is not None: + micros = [m1.merge(m2) for m1, m2 in zip(flux_micros, direct_micros)] + elif rr_tally is not None: + micros = direct_micros + else: + micros = flux_micros + # Reset tallies model.tallies = original_tallies @@ -484,6 +530,79 @@ class MicroXS: return cls(data, nuclides, reactions) + def merge(self, other: Self, prefer: str = 'other') -> Self: + """Merge two MicroXS objects by taking the union of nuclides/reactions. + + If the two objects contain overlapping nuclide/reaction entries, values + from `other` will overwrite values from `self` when `prefer='other'`. + When `prefer='self'`, values from `self` are retained for overlapping + entries, and values from `other` are used only for non-overlapping + entries. + + Parameters + ---------- + other : MicroXS + Other MicroXS instance to merge with this one. + prefer : {"other", "self"} + Which instance's data should take precedence on overlap. + + Returns + ------- + MicroXS + New instance containing the merged data. + """ + check_value('prefer', prefer, {'other', 'self'}) + + # Require same number of energy groups + if self.data.shape[2] != other.data.shape[2]: + raise ValueError( + 'Cannot merge MicroXS with different number of energy groups: ' + f"{self.data.shape[2]} vs {other.data.shape[2]}. Ensure that " + 'both were generated with consistent group structures and ' + 'treatments (e.g., both multigroup or both collapsed).' + ) + + # Build unified axes preserving order (self first, then other's new) + new_nuclides = list(self.nuclides) + for nuc in other.nuclides: + if nuc not in self._index_nuc: + new_nuclides.append(nuc) + new_reactions = list(self.reactions) + for rx in other.reactions: + if rx not in self._index_rx: + new_reactions.append(rx) + + # Allocate and fill from self (self's nuclides/reactions map to the + # first indices of new_nuclides/new_reactions by construction) + groups = self.data.shape[2] + data = np.zeros((len(new_nuclides), len(new_reactions), groups)) + idx_n = {nuc: i for i, nuc in enumerate(new_nuclides)} + idx_r = {rx: i for i, rx in enumerate(new_reactions)} + + n_self = len(self.nuclides) + r_self = len(self.reactions) + data[:n_self, :r_self] = self.data + + # Build destination index arrays for other's nuclides/reactions + dst_n = np.array([idx_n[nuc] for nuc in other.nuclides]) + dst_r = np.array([idx_r[rx] for rx in other.reactions]) + + # Copy from other, respecting precedence + if prefer == 'other': + data[np.ix_(dst_n, dst_r)] = other.data + else: + # Copy only entries where nuc or rx is absent from self + nuc_is_new = np.array( + [nuc not in self._index_nuc for nuc in other.nuclides]) + rx_is_new = np.array( + [rx not in self._index_rx for rx in other.reactions]) + mask = nuc_is_new[:, np.newaxis] | rx_is_new[np.newaxis, :] + src_i, src_j = np.where(mask) + if src_i.size: + data[dst_n[src_i], dst_r[src_j]] = other.data[src_i, src_j] + + return MicroXS(data, new_nuclides, new_reactions) + def write_microxs_hdf5( micros: Sequence[MicroXS], diff --git a/tests/regression_tests/microxs/test.py b/tests/regression_tests/microxs/test.py index 70833bb39c..781be3ef7f 100644 --- a/tests/regression_tests/microxs/test.py +++ b/tests/regression_tests/microxs/test.py @@ -34,6 +34,7 @@ def model(): [ ("materials", "direct"), ("materials", "flux"), + ("materials", "hybrid"), ("mesh", "direct"), ("mesh", "flux"), ] @@ -49,12 +50,21 @@ def test_from_model(model, domain_type, rr_mode): domains = mesh nuclides = ['U235', 'O16', 'Xe135'] kwargs = { - 'reaction_rate_mode': rr_mode, 'chain_file': CHAIN_FILE, 'path_statepoint': 'neutron_transport.h5', } if rr_mode == 'flux': + kwargs['reaction_rate_mode'] = 'flux' kwargs['energies'] = 'CASMO-40' + elif rr_mode == 'hybrid': + kwargs['reaction_rate_mode'] = 'flux' + kwargs['energies'] = 'CASMO-40' + kwargs['reaction_rate_opts'] = { + 'nuclides': ['U235'], + 'reactions': ['fission'], + } + else: + kwargs['reaction_rate_mode'] = rr_mode _, test_xs = get_microxs_and_flux(model, domains, nuclides, **kwargs) if config['update']: test_xs[0].to_csv(f'test_reference_{domain_type}_{rr_mode}.csv') diff --git a/tests/regression_tests/microxs/test_reference_materials_hybrid.csv b/tests/regression_tests/microxs/test_reference_materials_hybrid.csv new file mode 100644 index 0000000000..6d17a5e2be --- /dev/null +++ b/tests/regression_tests/microxs/test_reference_materials_hybrid.csv @@ -0,0 +1,7 @@ +nuclides,reactions,groups,xs +U235,"(n,gamma)",1,0.1500301670375847 +U235,fission,1,1.2578145727734291 +O16,"(n,gamma)",1,0.00012069778439640312 +O16,fission,1,0.0 +Xe135,"(n,gamma)",1,0.014820264774863558 +Xe135,fission,1,0.0 diff --git a/tests/unit_tests/test_deplete_microxs.py b/tests/unit_tests/test_deplete_microxs.py index 5762a8511b..340ba61632 100644 --- a/tests/unit_tests/test_deplete_microxs.py +++ b/tests/unit_tests/test_deplete_microxs.py @@ -6,12 +6,15 @@ to a custom file with new depletion_chain node from os import remove from pathlib import Path +from unittest.mock import patch import pytest -from openmc.deplete import MicroXS +import openmc +from openmc.deplete import MicroXS, get_microxs_and_flux import numpy as np ONE_GROUP_XS = Path(__file__).parents[1] / "micro_xs_simple.csv" +CHAIN_FILE = Path(__file__).parents[1] / "chain_simple.xml" def test_from_array(): @@ -124,3 +127,127 @@ def test_microxs_zero_flux(): # All microscopic cross sections should be zero assert np.all(microxs.data == 0.0) + + +def test_hybrid_tally_setup(): + """In hybrid mode a 1-group RR tally is added alongside the flux tally.""" + # Create a simple model with one material and a few nuclides for testing + model = openmc.Model() + mat = openmc.Material(components={'U235': 1.0, 'O16': 2.0}) + sphere = openmc.Sphere(r=10.0, boundary_type='vacuum') + cell = openmc.Cell(region=-sphere, fill=mat) + model.geometry = openmc.Geometry([cell]) + model.settings.batches = 2 + model.settings.particles = 10 + + # Define 2-group energy structure for the test + energies = [0., 0.625, 2.0e7] + + # Function to replace Model.run and capture the tallies that were created + captured = {} + def capture_run(**kwargs): + captured['tallies'] = list(model.tallies) + raise StopIteration + + # Call get_microxs_and_flux but replace Model.run with a function that + # captures the tallies and raises StopIteration to exit early + with patch.object(model, 'run', side_effect=capture_run): + with pytest.raises(StopIteration): + get_microxs_and_flux( + model, [mat], + nuclides=['U235', 'O16'], + reactions=['fission', '(n,gamma)'], + energies=energies, + reaction_rate_mode='flux', + reaction_rate_opts={'nuclides': ['U235'], 'reactions': ['fission']}, + chain_file=CHAIN_FILE, + ) + + # Check that both tallies were created with the expected properties + tally_names = [t.name for t in captured['tallies']] + assert 'MicroXS flux' in tally_names + assert 'MicroXS RR' in tally_names + + # Check that the RR tally has the expected nuclides and reactions + rr = next(t for t in captured['tallies'] if t.name == 'MicroXS RR') + assert rr.nuclides == ['U235'] + assert rr.scores == ['fission'] + + # RR tally must use a 1-group energy filter spanning the full energy range + ef = next(f for f in rr.filters if isinstance(f, openmc.EnergyFilter)) + assert len(ef.values) == 2 + assert ef.values[0] == pytest.approx(energies[0]) + assert ef.values[-1] == pytest.approx(energies[-1]) + +# --------------------------------------------------------------------------- +# Tests for MicroXS.merge() +# --------------------------------------------------------------------------- + +def _make_microxs(nuclides, reactions, values, groups=1): + """Helper: build a MicroXS from a flat list of values (nuclide-major order).""" + data = np.array(values, dtype=float).reshape( + len(nuclides), len(reactions), groups) + return MicroXS(data, nuclides, reactions) + + +def test_merge_disjoint(): + """Merging two MicroXS with no overlapping nuclides or reactions.""" + m1 = _make_microxs(['U235', 'U238'], ['fission', '(n,gamma)'], + [1., 2., 3., 4.]) + m2 = _make_microxs(['Pu239'], ['(n,2n)'], [5.]) + + merged = m1.merge(m2) + + assert merged.nuclides == ['U235', 'U238', 'Pu239'] + assert merged.reactions == ['fission', '(n,gamma)', '(n,2n)'] + assert merged.data.shape == (3, 3, 1) + + # Self data preserved + assert merged['U235', 'fission'] == pytest.approx([1.]) + assert merged['U238', '(n,gamma)'] == pytest.approx([4.]) + # New data from other + assert merged['Pu239', '(n,2n)'] == pytest.approx([5.]) + # Cross-terms that had no data should be zero + assert merged['U235', '(n,2n)'] == pytest.approx([0.]) + assert merged['Pu239', 'fission'] == pytest.approx([0.]) + + +def test_merge_prefer_other(): + """prefer='other': other overwrites shared entries, adds new ones.""" + # m1: U235 and U238, reactions fission and (n,gamma) + m1 = _make_microxs(['U235', 'U238'], ['fission', '(n,gamma)'], + [1., 2., 3., 4.]) + # m2: only U235, reactions fission (overlap) and (n,2n) (new) + m2 = _make_microxs(['U235'], ['fission', '(n,2n)'], [9., 5.]) + + merged = m1.merge(m2) + + # Nuclide/reaction sets + assert set(merged.nuclides) == {'U235', 'U238'} + assert set(merged.reactions) == {'fission', '(n,gamma)', '(n,2n)'} + + # U235/fission overwritten by m2 + assert merged['U235', 'fission'] == pytest.approx([9.]) + # U235/(n,gamma) untouched + assert merged['U235', '(n,gamma)'] == pytest.approx([2.]) + # New reaction added from m2 + assert merged['U235', '(n,2n)'] == pytest.approx([5.]) + # U238 data preserved + assert merged['U238', 'fission'] == pytest.approx([3.]) + assert merged['U238', '(n,gamma)'] == pytest.approx([4.]) + # U238/(n,2n) not in either → zero + assert merged['U238', '(n,2n)'] == pytest.approx([0.]) + + +def test_merge_prefer_self(): + """prefer='self': shared pairs keep self's value; new entries use other's value.""" + m1 = _make_microxs(['U235', 'U238'], ['fission', '(n,gamma)'], + [1., 2., 3., 4.]) + m2 = _make_microxs(['U235'], ['fission', '(n,2n)'], [9., 5.]) + + merged = m1.merge(m2, prefer='self') + + # U235/fission: self wins + assert merged['U235', 'fission'] == pytest.approx([1.]) + # U235/(n,2n): other used + assert merged['U235', '(n,2n)'] == pytest.approx([5.]) From 53d98ce71acddd028a8361bf966aa8d6be204cfd Mon Sep 17 00:00:00 2001 From: Marco De Pietri Date: Tue, 3 Mar 2026 10:48:56 -0500 Subject: [PATCH 29/38] Add method on Material for computing photon contact dose rate (#3700) Co-authored-by: Paul Romano --- docs/source/pythonapi/data.rst | 2 + openmc/data/__init__.py | 4 +- .../data/{effective_dose => dose}/__init__.py | 0 openmc/data/{effective_dose => dose}/dose.py | 0 .../icrp116/electrons.txt | 0 .../icrp116/helium_ions.txt | 0 .../icrp116/negative_muons.txt | 0 .../icrp116/negative_pions.txt | 0 .../icrp116/neutrons.txt | 0 .../icrp116/photons.txt | 0 .../icrp116/photons_kerma.txt | 0 .../icrp116/positive_muons.txt | 0 .../icrp116/positive_pions.txt | 0 .../icrp116/positrons.txt | 0 .../icrp116/protons.txt | 0 .../icrp74/generate_photon_effective_dose.py | 0 .../icrp74/neutrons.txt | 0 .../icrp74/photons.txt | 0 openmc/data/dose/mass_attenuation.h5 | Bin 0 -> 135056 bytes openmc/data/dose/mass_attenuation.py | 153 ++++++++++++++ openmc/material.py | 191 +++++++++++++++++- pyproject.toml | 2 +- .../unit_tests/test_data_mass_attenuation.py | 53 +++++ tests/unit_tests/test_material.py | 55 +++++ tests/unit_tests/test_mesh.py | 1 + 25 files changed, 457 insertions(+), 4 deletions(-) rename openmc/data/{effective_dose => dose}/__init__.py (100%) rename openmc/data/{effective_dose => dose}/dose.py (100%) rename openmc/data/{effective_dose => dose}/icrp116/electrons.txt (100%) rename openmc/data/{effective_dose => dose}/icrp116/helium_ions.txt (100%) rename openmc/data/{effective_dose => dose}/icrp116/negative_muons.txt (100%) rename openmc/data/{effective_dose => dose}/icrp116/negative_pions.txt (100%) rename openmc/data/{effective_dose => dose}/icrp116/neutrons.txt (100%) rename openmc/data/{effective_dose => dose}/icrp116/photons.txt (100%) rename openmc/data/{effective_dose => dose}/icrp116/photons_kerma.txt (100%) rename openmc/data/{effective_dose => dose}/icrp116/positive_muons.txt (100%) rename openmc/data/{effective_dose => dose}/icrp116/positive_pions.txt (100%) rename openmc/data/{effective_dose => dose}/icrp116/positrons.txt (100%) rename openmc/data/{effective_dose => dose}/icrp116/protons.txt (100%) rename openmc/data/{effective_dose => dose}/icrp74/generate_photon_effective_dose.py (100%) rename openmc/data/{effective_dose => dose}/icrp74/neutrons.txt (100%) rename openmc/data/{effective_dose => dose}/icrp74/photons.txt (100%) create mode 100644 openmc/data/dose/mass_attenuation.h5 create mode 100644 openmc/data/dose/mass_attenuation.py create mode 100644 tests/unit_tests/test_data_mass_attenuation.py diff --git a/docs/source/pythonapi/data.rst b/docs/source/pythonapi/data.rst index 1eaf90c972..9d47430f75 100644 --- a/docs/source/pythonapi/data.rst +++ b/docs/source/pythonapi/data.rst @@ -71,6 +71,8 @@ Core Functions isotopes kalbach_slope linearize + mass_attenuation_coefficient + mass_energy_absorption_coefficient thin water_density zam diff --git a/openmc/data/__init__.py b/openmc/data/__init__.py index a45d026a03..9b38d758ec 100644 --- a/openmc/data/__init__.py +++ b/openmc/data/__init__.py @@ -35,4 +35,6 @@ from .grid import * from .function import * from .vectfit import * -from .effective_dose.dose import dose_coefficients +from .dose.dose import dose_coefficients +from .dose.mass_attenuation import \ + mass_energy_absorption_coefficient, mass_attenuation_coefficient diff --git a/openmc/data/effective_dose/__init__.py b/openmc/data/dose/__init__.py similarity index 100% rename from openmc/data/effective_dose/__init__.py rename to openmc/data/dose/__init__.py diff --git a/openmc/data/effective_dose/dose.py b/openmc/data/dose/dose.py similarity index 100% rename from openmc/data/effective_dose/dose.py rename to openmc/data/dose/dose.py diff --git a/openmc/data/effective_dose/icrp116/electrons.txt b/openmc/data/dose/icrp116/electrons.txt similarity index 100% rename from openmc/data/effective_dose/icrp116/electrons.txt rename to openmc/data/dose/icrp116/electrons.txt diff --git a/openmc/data/effective_dose/icrp116/helium_ions.txt b/openmc/data/dose/icrp116/helium_ions.txt similarity index 100% rename from openmc/data/effective_dose/icrp116/helium_ions.txt rename to openmc/data/dose/icrp116/helium_ions.txt diff --git a/openmc/data/effective_dose/icrp116/negative_muons.txt b/openmc/data/dose/icrp116/negative_muons.txt similarity index 100% rename from openmc/data/effective_dose/icrp116/negative_muons.txt rename to openmc/data/dose/icrp116/negative_muons.txt diff --git a/openmc/data/effective_dose/icrp116/negative_pions.txt b/openmc/data/dose/icrp116/negative_pions.txt similarity index 100% rename from openmc/data/effective_dose/icrp116/negative_pions.txt rename to openmc/data/dose/icrp116/negative_pions.txt diff --git a/openmc/data/effective_dose/icrp116/neutrons.txt b/openmc/data/dose/icrp116/neutrons.txt similarity index 100% rename from openmc/data/effective_dose/icrp116/neutrons.txt rename to openmc/data/dose/icrp116/neutrons.txt diff --git a/openmc/data/effective_dose/icrp116/photons.txt b/openmc/data/dose/icrp116/photons.txt similarity index 100% rename from openmc/data/effective_dose/icrp116/photons.txt rename to openmc/data/dose/icrp116/photons.txt diff --git a/openmc/data/effective_dose/icrp116/photons_kerma.txt b/openmc/data/dose/icrp116/photons_kerma.txt similarity index 100% rename from openmc/data/effective_dose/icrp116/photons_kerma.txt rename to openmc/data/dose/icrp116/photons_kerma.txt diff --git a/openmc/data/effective_dose/icrp116/positive_muons.txt b/openmc/data/dose/icrp116/positive_muons.txt similarity index 100% rename from openmc/data/effective_dose/icrp116/positive_muons.txt rename to openmc/data/dose/icrp116/positive_muons.txt diff --git a/openmc/data/effective_dose/icrp116/positive_pions.txt b/openmc/data/dose/icrp116/positive_pions.txt similarity index 100% rename from openmc/data/effective_dose/icrp116/positive_pions.txt rename to openmc/data/dose/icrp116/positive_pions.txt diff --git a/openmc/data/effective_dose/icrp116/positrons.txt b/openmc/data/dose/icrp116/positrons.txt similarity index 100% rename from openmc/data/effective_dose/icrp116/positrons.txt rename to openmc/data/dose/icrp116/positrons.txt diff --git a/openmc/data/effective_dose/icrp116/protons.txt b/openmc/data/dose/icrp116/protons.txt similarity index 100% rename from openmc/data/effective_dose/icrp116/protons.txt rename to openmc/data/dose/icrp116/protons.txt diff --git a/openmc/data/effective_dose/icrp74/generate_photon_effective_dose.py b/openmc/data/dose/icrp74/generate_photon_effective_dose.py similarity index 100% rename from openmc/data/effective_dose/icrp74/generate_photon_effective_dose.py rename to openmc/data/dose/icrp74/generate_photon_effective_dose.py diff --git a/openmc/data/effective_dose/icrp74/neutrons.txt b/openmc/data/dose/icrp74/neutrons.txt similarity index 100% rename from openmc/data/effective_dose/icrp74/neutrons.txt rename to openmc/data/dose/icrp74/neutrons.txt diff --git a/openmc/data/effective_dose/icrp74/photons.txt b/openmc/data/dose/icrp74/photons.txt similarity index 100% rename from openmc/data/effective_dose/icrp74/photons.txt rename to openmc/data/dose/icrp74/photons.txt diff --git a/openmc/data/dose/mass_attenuation.h5 b/openmc/data/dose/mass_attenuation.h5 new file mode 100644 index 0000000000000000000000000000000000000000..f62785140c3ed50eac4f54a7253ffcb26b3526d4 GIT binary patch literal 135056 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 ndarray +# Table shape: (N, 2) with columns [Energy (MeV), μen/ρ (cm^2/g)] +_MUEN_TABLES = { + ("nist126", "air"): _NIST126_AIR, +} + + +def mass_energy_absorption_coefficient( + material: str, data_source: str = "nist126" +) -> Tabulated1D: + """Return the mass energy-absorption coefficient as a function of energy. + + The mass energy-absorption coefficient, :math:`\mu_\text{en}/\rho`, is + defined as the fraction of incident photon energy absorbed in a material per + unit mass less the energy carried away by scattered photons. It is obtained + from `NIST Standard Reference Database 126 + `_: X-Ray Mass Attenuation Coefficients. + + Parameters + ---------- + material : {'air'} + Material compound for which to load coefficients. + data_source : {'nist126'} + Source library. + + Returns + ------- + Tabulated1D + Mass energy-absorption coefficient [cm^2/g] as a function of photon + energy [eV], using log-log interpolation. + + """ + cv.check_value("material", material, {"air"}) + cv.check_value("data_source", data_source, {"nist126"}) + + key = (data_source, material) + if key not in _MUEN_TABLES: + available = sorted({m for (ds, m) in _MUEN_TABLES.keys() if ds == data_source}) + raise ValueError( + f"No mass energy-absorption data for '{material}' in data source " + f"'{data_source}'. Available materials: {available}" + ) + + data = _MUEN_TABLES[key] + energy = data[:, 0].copy() * EV_PER_MEV # MeV -> eV + mu_en_coeffs = data[:, 1].copy() + return Tabulated1D(energy, mu_en_coeffs, + breakpoints=[len(energy)], interpolation=[5]) + + +# Used in mass_attenuation_coefficient function as a cache. +# Maps atomic number Z (int) -> Tabulated1D of (mu/rho) [cm^2/g] vs E [eV] +_MASS_ATTENUATION: dict[int, object] = {} + + +def mass_attenuation_coefficient(element): + """Return the photon mass attenuation coefficient as a function of energy. + + The mass energy-absorption coefficient, :math:`\mu_\text{en}/\rho`, is + defined as the fraction of incident photon energy absorbed in a material per + unit mass. Values for each element are obtained from `NIST Standard + Reference Database 8 `_: XCOM Photon Cross + Sections Database. + + Parameters + ---------- + element : str or int + Element symbol (e.g., 'Fe') or atomic number (e.g., 26). + + Returns + ------- + Tabulated1D + Mass attenuation coefficient [cm^2/g] as a function of photon energy + [eV], using log-log interpolation. + + """ + if not _MASS_ATTENUATION: + data_file = Path(__file__).with_name('mass_attenuation.h5') + with h5py.File(data_file, 'r') as f: + for key, dataset in f.items(): + energies, mu_rho = dataset[()] # shape (2, N) + _MASS_ATTENUATION[int(key)] = Tabulated1D( + energies, mu_rho, + breakpoints=[len(energies)], + interpolation=[5] # log-log + ) + + # Resolve element argument to atomic number + if isinstance(element, str): + if element not in ATOMIC_NUMBER: + raise ValueError(f"'{element}' is not a recognized element symbol") + Z = ATOMIC_NUMBER[element] + else: + Z = int(element) + + if Z not in _MASS_ATTENUATION: + raise ValueError(f"No mass attenuation data available for Z={Z}") + + return _MASS_ATTENUATION[Z] diff --git a/openmc/material.py b/openmc/material.py index 735a057432..239bc01f77 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections import defaultdict, namedtuple, Counter from collections.abc import Iterable from copy import deepcopy +from functools import reduce from numbers import Real from pathlib import Path import re @@ -22,8 +23,10 @@ from .mixin import IDManagerMixin from .utility_funcs import input_path from . import waste from openmc.checkvalue import PathLike -from openmc.stats import Univariate, Discrete, Mixture -from openmc.data.data import _get_element_symbol +from openmc.stats import Univariate, Discrete, Mixture, Tabular +from openmc.data.data import _get_element_symbol, JOULE_PER_EV +from openmc.data.function import Tabulated1D +from openmc.data import mass_energy_absorption_coefficient, dose_coefficients # Units for density supported by OpenMC @@ -409,6 +412,190 @@ class Material(IDManagerMixin): return combined + def get_photon_contact_dose_rate( + self, + dose_quantity: str = "absorbed-air", + build_up: float = 2.0, + by_nuclide: bool = False + ) -> float | dict[str, float]: + """Compute the photon contact dose rate (CDR) produced by radioactive decay + of the material. + + The contact dose rate is calculated from decay photon energy spectra for + each nuclide in the material, combined with photon mass attenuation data + for the material and the appropriate response function for the dose quantity. + A slab-geometry approximation and a photon build-up factor are used. + + Absorbed-air dose: + The approach follows the FISPACT-II manual (UKAEA-CCFE-RE(21)02 - May 2021). + Appendix C.7.1. + This method integrates over the photon energy: + + (B/2) * (mu_en_air(E) / mu_material(E)) * E * S(E) + + Effective dose: + The approach uses ICRP-116 effective dose coefficients to convert the photon + fluence due to decay photons to effective dose. + This method integrates over the photon energy: + + (B/2) * (h_e(E) / mu_material(E)) * S(E) + + where: + - mu_en_air(E) is the air mass energy-absorption coefficient, + - mu_material(E) is the photon mass attenuation coefficient of the material, + - S(E) is the photon emission spectrum per atom, + - h_e(E) is the ICRP-116 effective dose coefficient, + - B is the build-up factor, + - E is the photon energy. + + Parameters + ---------- + dose_quantity : {'absorbed-air', 'effective'}, optional + Specifies the dose quantity to be calculated. + The only supported options are 'absorbed-air' which implements the methodology + from FISPACT-II, and 'effective' which uses ICRP-116 effective dose coefficients. + build_up : float, optional. The default value is 2.0 as suggested in the FISPACT-II + manual. + by_nuclide : bool, optional + Specifies if the cdr should be returned for the material as a + whole or per nuclide. Default is False. + + Limitations + ---------- + This method does not implement correction from Bremsstrahlung particles which can be + relevant at close distances. + In addition, it computes the gamma contact dose rate only for the unstable nuclides + for which the radiation source specification is present in the chain file. + + Returns + ------- + cdr : float or dict[str, float] + Contact Dose Rate due to decay photons. + 'absorbed-air': returns the absorbed dose in air [Gy/hr]. + 'effective': returns the effective dose [Sv/hr]. + """ + + cv.check_type("by_nuclide", by_nuclide, bool) + cv.check_type("dose_quantity", dose_quantity, str) + cv.check_value("dose_quantity", dose_quantity, {'absorbed-air', 'effective'}) + cv.check_type("build_up", build_up, Real) + cv.check_greater_than("build_up", build_up, 0.0) + + nuc_densities = self.get_nuclide_atom_densities() + if not nuc_densities: + raise ValueError("Material has no nuclides; cannot compute mass attenuation") + + # Collect partial mass densities ρ_i [g/cm³] and elemental mass + # attenuation coefficients µ_i/ρ_i [cm²/g] per nuclide + nuc_attenuation = [] + for nuc, atom_density_bcm in nuc_densities.items(): + Z = openmc.data.zam(nuc)[0] + mu_over_rho = openmc.data.mass_attenuation_coefficient(Z) + rho_i = ( + atom_density_bcm * 1.0e24 + * openmc.data.atomic_mass(nuc) / openmc.data.AVOGADRO + ) + nuc_attenuation.append((rho_i, mu_over_rho)) + + # Build union energy grid across all nuclides + mu_e_vals = reduce(np.union1d, [t.x for _, t in nuc_attenuation]) + + # Build the material linear attenuation coefficient µ_material(E) [cm⁻¹] + # as the sum of ρ_i * (µ_i/ρ_i)(E) over all nuclides + mu_material_vals = np.zeros(len(mu_e_vals)) + for rho_i, mu_over_rho in nuc_attenuation: + mu_material_vals += rho_i * mu_over_rho(mu_e_vals) + mu_material = Tabulated1D( + mu_e_vals, mu_material_vals, breakpoints=[len(mu_e_vals)], interpolation=[5]) + + # CDR computation + cdr = {} + + geometry_factor_slab = 0.5 + + # ancillary conversion factors for clarity + seconds_per_hour = 3600.0 + grams_per_kg = 1000.0 + sv_per_psv = 1e-12 + + if dose_quantity == 'absorbed-air': + # mu_en/rho for air [cm²/g] as a function of energy [eV] + response_f = mass_energy_absorption_coefficient("air", data_source="nist126") + + # Factor to convert [eV cm²/(b g s)] to [Gy/h] + multiplier = (build_up * geometry_factor_slab * seconds_per_hour + * grams_per_kg * 1e24 * JOULE_PER_EV) + + elif dose_quantity == 'effective': + # effective dose as a function of photon fluence [pSv cm²] + response_f_x, response_f_y = dose_coefficients( + "photon", geometry='AP', data_source='icrp116') + response_f = Tabulated1D(response_f_x, response_f_y, breakpoints=[ + len(response_f_x)], interpolation=[5]) + + # Convert [pSv cm²/(b-s)] to [Sv/h] + multiplier = (build_up * geometry_factor_slab * seconds_per_hour + * sv_per_psv * 1e24) + + for nuc, nuc_atoms_per_bcm in self.get_nuclide_atom_densities().items(): + photon_source_per_atom = openmc.data.decay_photon_energy(nuc) + + # nuclides with no contribution + if photon_source_per_atom is None or nuc_atoms_per_bcm <= 0.0: + cdr[nuc] = 0.0 + continue + + if not isinstance(photon_source_per_atom, (Discrete, Tabular)): + raise ValueError( + f"Unknown decay photon energy data type for nuclide {nuc}" + f"value returned: {type(photon_source_per_atom)}" + ) + + e_vals = photon_source_per_atom.x + p_vals = photon_source_per_atom.p + + # Construct list of energies from (photon source, response function, + # mu_en_air) for clipping to common energy range + e_lists = [e_vals, response_f.x, mu_e_vals] + + # clip distributions for values outside the tabulated values + left_bound = max(a.min() for a in e_lists) + right_bound = min(a.max() for a in e_lists) + + mask = (e_vals >= left_bound) & (e_vals <= right_bound) + e_vals = e_vals[mask] + p_vals = p_vals[mask] + + if isinstance(photon_source_per_atom, Tabular): + # limit the computation to the tabulated mu_en_air range + e_union = reduce(np.union1d, e_lists) + e_union = e_union[(e_union >= left_bound) & (e_union <= right_bound)] + if len(e_union) < 2: + raise ValueError("Not enough overlapping energy points to compute CDR") + + # Histogram interpolation: each new point inherits the value of + # the nearest original point to its left + p_vals = p_vals[np.searchsorted(e_vals, e_union, side='right') - 1] + e_vals = e_union + + mu_vals = mu_material(e_vals) + if dose_quantity == 'absorbed-air': + # Compute (µ_en_air(E) / µ_material(E)) * E * S(E) + integrand = (response_f(e_vals) / mu_vals) * p_vals * e_vals + elif dose_quantity == 'effective': + # Compute (h_e(E) / µ_material(E)) * S(E) + integrand = (response_f(e_vals) / mu_vals) * p_vals + + if isinstance(photon_source_per_atom, Discrete): + cdr_nuc = np.sum(integrand) + elif isinstance(photon_source_per_atom, Tabular): + cdr_nuc = np.trapezoid(integrand, e_vals) + + # Compute air-absorbed dose [Gy/h] or effective dose [Sv/h] + cdr[nuc] = float(cdr_nuc * nuc_atoms_per_bcm * multiplier) + + return cdr if by_nuclide else sum(cdr.values()) + @classmethod def from_hdf5(cls, group: h5py.Group) -> Material: """Create material from HDF5 group diff --git a/pyproject.toml b/pyproject.toml index b2b5ff5e40..bf40113445 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,7 +69,7 @@ include = ['openmc*'] exclude = ['tests*'] [tool.setuptools.package-data] -"openmc.data.effective_dose" = ["**/*.txt"] +"openmc.data.dose" = ["**/*.txt"] "openmc.data" = ["*.txt", "*.DAT", "*.json", "*.h5"] "openmc.lib" = ["libopenmc.dylib", "libopenmc.so"] diff --git a/tests/unit_tests/test_data_mass_attenuation.py b/tests/unit_tests/test_data_mass_attenuation.py new file mode 100644 index 0000000000..0fe12a7db2 --- /dev/null +++ b/tests/unit_tests/test_data_mass_attenuation.py @@ -0,0 +1,53 @@ +from pytest import approx, raises + +from openmc.data import mass_energy_absorption_coefficient, mass_attenuation_coefficient +from openmc.data.function import Tabulated1D + + +def test_mass_attenuation_type(): + mu = mass_attenuation_coefficient(26) # Fe + assert isinstance(mu, Tabulated1D) + + +def test_mass_attenuation_spot_values(): + # Spot checks for Fe (Z=26) against NIST data: first/last tabulated points + # and a mid-range value at 1 MeV + mu = mass_attenuation_coefficient(26) + assert mu(1e3) == approx(9085.0) + assert mu(1e6) == approx(0.05995) + assert mu(2e7) == approx(0.03224) + + +def test_mass_attenuation_caching(): + # Repeated calls with the same Z should return the identical object + mu1 = mass_attenuation_coefficient(26) + mu2 = mass_attenuation_coefficient('Fe') + assert mu1 is mu2 + + +def test_mass_attenuation_invalid_z(): + with raises(ValueError, match="Z=0"): + mass_attenuation_coefficient(0) + with raises(ValueError, match="Z=200"): + mass_attenuation_coefficient(200) + + +def test_mass_energy_absorption_type(): + # Spot checks on values from NIST tables + mu_en = mass_energy_absorption_coefficient("air") + assert isinstance(mu_en, Tabulated1D) + + +def test_mass_energy_absorption_spot_values(): + mu_en = mass_energy_absorption_coefficient("air") + assert mu_en(1e3) == approx(3.599e3) + assert mu_en(10.e3) == approx(4.742) + assert mu_en(2e7) == approx(1.311e-2) + + +def test_mass_energy_absorption_invalid(): + # Invalid material/data_source should raise an exception + with raises(ValueError): + mass_energy_absorption_coefficient("pasta") + with raises(ValueError): + mass_energy_absorption_coefficient("air", data_source="nist000") diff --git a/tests/unit_tests/test_material.py b/tests/unit_tests/test_material.py index 0b1b5fce24..58cd4d563d 100644 --- a/tests/unit_tests/test_material.py +++ b/tests/unit_tests/test_material.py @@ -826,3 +826,58 @@ def test_material_from_constructor(): assert mat2.density == 1e-7 assert mat2.density_units == "g/cm3" assert mat2.nuclides == [] + + +def test_get_photon_contact_dose_rate(): + # Set chain file for testing + openmc.config['chain_file'] = Path(__file__).parents[1] / 'chain_simple.xml' + + # A purely stable material (Fe) should give zero dose + m_stable = openmc.Material() + m_stable.add_element('Fe', 1.0) + m_stable.set_density('g/cm3', 7.87) + assert m_stable.get_photon_contact_dose_rate('absorbed-air') == 0.0 + assert m_stable.get_photon_contact_dose_rate('effective') == 0.0 + + # I135 has a Discrete photon source (lines) + m_i135 = openmc.Material() + m_i135.add_nuclide('I135', 1.0) + m_i135.set_density('atom/b-cm', 1.0) + + cdr_abs = m_i135.get_photon_contact_dose_rate('absorbed-air') + cdr_eff = m_i135.get_photon_contact_dose_rate('effective') + assert cdr_abs == pytest.approx(6.091547e10, rel=1e-4) # [Gy/h] + assert cdr_eff == pytest.approx(6.102167e10, rel=1e-4) # [Sv/h] + + # Xe135 has a Tabular photon source (continuous distribution) + m_xe135 = openmc.Material() + m_xe135.add_nuclide('Xe135', 1.0) + m_xe135.set_density('atom/b-cm', 1.0) + + cdr_xe_abs = m_xe135.get_photon_contact_dose_rate('absorbed-air') + cdr_xe_eff = m_xe135.get_photon_contact_dose_rate('effective') + assert cdr_xe_abs == pytest.approx(7.886077e8, rel=1e-4) # [Gy/h] + assert cdr_xe_eff == pytest.approx(9.488298e8, rel=1e-4) # [Sv/h] + + # by_nuclide=True should return a dict whose values sum to the total + cdr_by_nuc = m_i135.get_photon_contact_dose_rate('absorbed-air', by_nuclide=True) + assert isinstance(cdr_by_nuc, dict) + assert 'I135' in cdr_by_nuc + assert sum(cdr_by_nuc.values()) == pytest.approx(cdr_abs) + + # For a mixed material the sum over nuclides must equal the total + m_mix = openmc.Material() + m_mix.add_nuclide('I135', 0.5) + m_mix.add_nuclide('Xe135', 0.5) + m_mix.set_density('atom/b-cm', 1.0) + cdr_mix_total = m_mix.get_photon_contact_dose_rate('absorbed-air') + cdr_mix_nuc = m_mix.get_photon_contact_dose_rate('absorbed-air', by_nuclide=True) + assert sum(cdr_mix_nuc.values()) == pytest.approx(cdr_mix_total) + + # Input validation + with pytest.raises(ValueError): + m_i135.get_photon_contact_dose_rate('invalid-quantity') + with pytest.raises(TypeError): + m_i135.get_photon_contact_dose_rate('absorbed-air', build_up='two') + with pytest.raises(ValueError): + m_i135.get_photon_contact_dose_rate('absorbed-air', build_up=-1.0) diff --git a/tests/unit_tests/test_mesh.py b/tests/unit_tests/test_mesh.py index 9d07eda0d9..aa8bcae5f1 100644 --- a/tests/unit_tests/test_mesh.py +++ b/tests/unit_tests/test_mesh.py @@ -612,6 +612,7 @@ def test_mesh_get_homogenized_materials(): @pytest.fixture def sphere_model(): + openmc.reset_auto_ids() # Model with three materials separated by planes x=0 and z=0 mats = [] for i in range(3): From b796afb5911860d6c22844108ee435fd93d7b5a9 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Wed, 4 Mar 2026 06:10:54 -0600 Subject: [PATCH 30/38] Correction to surface normal determination in SolidRayTrace plots (#3846) --- src/plot.cpp | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/plot.cpp b/src/plot.cpp index 5e3342dd68..b03c51e7e0 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -1847,7 +1847,10 @@ void PhongRay::on_intersection() // the normal or the diffuse lighting contribution reflected_ = true; result_color_ = plot_.colors_[hit_id]; - Direction to_light = plot_.light_location_ - r(); + // The ray has been advanced slightly past the boundary. Use an + // approximation to the actual hit point for stable normal/lighting. + Position r_hit = r() - TINY_BIT * u(); + Direction to_light = plot_.light_location_ - r_hit; to_light /= to_light.norm(); // TODO @@ -1868,12 +1871,22 @@ void PhongRay::on_intersection() // Get surface pointer const auto& surf = model::surfaces.at(surface_index()); - Direction normal = surf->normal(r_local()); + // The crossed surface may be on a higher coordinate level than the + // innermost local coordinates, so we check the surface's coordinate level + // to find the appropriate coordinate level to use for the normal + // calculation + int surf_level = boundary().coord_level() - 1; + // ensure surface level is within bounds of current coordinate stack + surf_level = std::max(0, std::min(surf_level, n_coord() - 1)); + + Position r_hit_level = + coord(surf_level).r() - TINY_BIT * coord(surf_level).u(); + Direction normal = surf->normal(r_hit_level); normal /= normal.norm(); - // Need to apply translations to find the normal vector in + // Need to apply rotations to find the normal vector in // the base level universe's coordinate system. - for (int lev = n_coord() - 2; lev >= 0; --lev) { + for (int lev = surf_level - 1; lev >= 0; --lev) { if (coord(lev + 1).rotated()) { const Cell& c {*model::cells[coord(lev).cell()]}; normal = normal.inverse_rotate(c.rotation_); From 70be6500038174a313d2245ae9825ddb375444c5 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Wed, 4 Mar 2026 14:17:48 +0200 Subject: [PATCH 31/38] Implement surface flux tallies (#3742) Co-authored-by: Paul Romano --- docs/source/io_formats/settings.rst | 17 +++ docs/source/methods/tallies.rst | 66 +++++++++- docs/source/usersguide/tallies.rst | 13 +- include/openmc/settings.h | 2 + include/openmc/tallies/tally_scoring.h | 12 +- openmc/settings.py | 55 ++++++++ src/finalize.cpp | 2 + src/particle.cpp | 25 ++-- src/settings.cpp | 10 ++ src/tallies/filter_surface.cpp | 6 +- src/tallies/tally.cpp | 48 +++++-- src/tallies/tally_scoring.cpp | 66 +++++++++- .../filter_musurface/inputs_true.dat | 2 +- .../filter_musurface/results_true.dat | 8 ++ .../regression_tests/filter_musurface/test.py | 5 +- .../surface_tally/results_true.dat | 16 +-- tests/regression_tests/surface_tally/test.py | 1 - tests/unit_tests/test_settings.py | 4 + tests/unit_tests/test_surface_flux.py | 120 ++++++++++++++++++ 19 files changed, 424 insertions(+), 54 deletions(-) create mode 100644 tests/unit_tests/test_surface_flux.py diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index db72984a63..e4ab0e169a 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -1234,6 +1234,23 @@ attributes/sub-elements: are not eligible to store any particles when using ``cell``, ``cellfrom`` or ``cellto`` attributes. It is recommended to use surface IDs instead. +------------------------------------ +```` Element +------------------------------------ + +The ```` element specifies the surface flux cosine cutoff. + + *Default*: 0.001 + +----------------------------------- +```` Element +----------------------------------- + +The ```` element specifies the surface flux cosine +substitution ratio. + + *Default*: 0.5 + ------------------------------ ```` Element ------------------------------ diff --git a/docs/source/methods/tallies.rst b/docs/source/methods/tallies.rst index 27a3f873ab..f2c22ab225 100644 --- a/docs/source/methods/tallies.rst +++ b/docs/source/methods/tallies.rst @@ -205,7 +205,71 @@ had a collision at every event. Thus, for tallies with outgoing-energy filters or for tallies of scattering moments (which require the scattering cosine of the change-in-angle), we must use an analog estimator. -.. TODO: Add description of surface current tallies +----------------------------------- +Surface-Integrated Flux and Current +----------------------------------- + +Surface tallies allow you to measure particle behavior as they cross specific +boundaries in your geometry. Unlike volume tallies, which integrate over a +volumetric region, surface tallies capture the current or flux passing through a +surface. Surface tallies are estimated using an analog estimator. + +Current Score +------------- + +When tallying the current across a surface, we simply count the weight of +particles that cross the surface of interest: + + +.. math:: + :label: analog-current-estimator + + J = \frac{1}{W} \sum_{i \in S} w_i. + +where :math:`J` is the area-integrated current passing through surface +:math:`S`, :math:`W` is the total starting weight of the particles, and +:math:`w_i` is the weight of the particle as it crosses the surface :math:`S`. + +Flux Score +---------- + +When tallying flux over a surface, we use the relationship between current and +flux: + + +.. math:: + :label: surface-flux-estimator + + \phi_S = \frac{1}{W} \sum_{i \in S} \frac{w_i}{|\mu|}. + +where :math:`\phi_S` is the area-integrated flux over surface :math:`S`, +:math:`W` is the total starting weight of the particles, :math:`w_i` is the +weight of the particle as it crosses the surface :math:`S` and :math:`\mu` is +the cosine of angle between the particle direction and the surface normal. + +This equation diverges when the particle crossing the surface is nearly parallel +to it (that is, as :math:`\mu` approaches zero). To remove this divergence, +OpenMC scores: + +.. math:: + :label: modified-surface-flux-estimator + + \phi_S = \frac{1}{W} \sum_{i \in S} w_i f(\mu). + +and the function :math:`f` is defined by: + +.. math:: + f(\mu) = \begin{cases} + \frac{1}{|\mu|} & |\mu| > \mu_\text{cut} \\ + \frac{1}{c\mu_\text{cut}} & |\mu| \le \mu_\text{cut} + \end{cases} + +where :math:`\mu_\text{cut}` is the grazing cosine cutoff and :math:`c` is the +cosine substitution ratio. The parameters :math:`\mu_\text{cut}` and :math:`c` +can be set by the user via the :attr:`openmc.Settings.surface_grazing_cutoff` +and :attr:`openmc.Settings.surface_grazing_ratio` attributes, respectively. The +default values for these parameters are 0.001 and 0.5 as recommended by +`Favorite, Thomas, and Booth `_. .. _tallies_statistics: diff --git a/docs/source/usersguide/tallies.rst b/docs/source/usersguide/tallies.rst index 5b5630556a..20a89cea7c 100644 --- a/docs/source/usersguide/tallies.rst +++ b/docs/source/usersguide/tallies.rst @@ -261,18 +261,21 @@ The following tables show all valid scores: +----------------------+---------------------------------------------------+ |Score | Description | +======================+===================================================+ - |current |Used in combination with a meshsurface filter: | + |current |It may not be used in conjunction with any other | + | |score except flux. | + | | | + | |When used in combination with a meshsurface filter:| | |Partial currents on the boundaries of each cell in | - | |a mesh. It may not be used in conjunction with any | - | |other score. Only energy and mesh filters may be | - | |used. | - | |Used in combination with a surface filter: | + | |a mesh. | + | | | + | |When used in combination with a surface filter: | | |Net currents on any surface previously defined in | | |the geometry. It may be used along with any other | | |filter, except meshsurface filters. | | |Surfaces can alternatively be defined with cell | | |from and cell filters thereby resulting in tallying| | |partial currents. | + | | | | |Units are particles per source particle. | +----------------------+---------------------------------------------------+ |events |Number of scoring events. Units are events per | diff --git a/include/openmc/settings.h b/include/openmc/settings.h index b369c99fef..19ef6e5d2a 100644 --- a/include/openmc/settings.h +++ b/include/openmc/settings.h @@ -181,6 +181,8 @@ extern int64_t ssw_cell_id; //!< Cell id for the surface source //!< write setting extern SSWCellType ssw_cell_type; //!< Type of option for the cell //!< argument of surface source write +extern double surface_grazing_cutoff; //!< surface flux cosine cutoff +extern double surface_grazing_ratio; //!< surface flux substitution ratio extern TemperatureMethod temperature_method; //!< method for choosing temperatures extern double diff --git a/include/openmc/tallies/tally_scoring.h b/include/openmc/tallies/tally_scoring.h index c3ab779e6a..29b3ec6e5b 100644 --- a/include/openmc/tallies/tally_scoring.h +++ b/include/openmc/tallies/tally_scoring.h @@ -101,11 +101,19 @@ void score_tracklength_tally(Particle& p, double distance); //! \param total_distance The distance in [cm] traveled by the particle void score_timed_tracklength_tally(Particle& p, double total_distance); -//! Score surface or mesh-surface tallies for particle currents. +//! Score mesh-surface tallies for particle currents. // //! \param p The particle being tracked //! \param tallies A vector of the indices of the tallies to score to -void score_surface_tally(Particle& p, const vector& tallies); +void score_meshsurface_tally(Particle& p, const vector& tallies); + +//! Score surface tallies for particle currents. +// +//! \param p The particle being tracked +//! \param tallies A vector of the indices of the tallies to score to +//! \param surf The surface being crossed +void score_surface_tally( + Particle& p, const vector& tallies, const Surface& surf); //! Score the pulse-height tally //! This is triggered at the end of every particle history diff --git a/openmc/settings.py b/openmc/settings.py index 2ad1d06e75..342fd5e53a 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -289,6 +289,14 @@ class Settings: :cellto: Cell ID used to determine if particles crossing identified surfaces are to be banked. Particles going to this declared cell will be banked (int) + surface_grazing_cutoff : float + Surface flux cosine cutoff. If not specified, the default value is + 0.001. For more information, see the surface tally section in the theory + manual. + surface_grazing_ratio : float + Surface flux cosine substitution ratio. If not specified, the default + value is 0.5. For more information, see the surface tally section in the + theory manual. survival_biasing : bool Indicate whether survival biasing is to be used tabular_legendre : dict @@ -399,6 +407,8 @@ class Settings: self._uniform_source_sampling = None self._seed = None self._stride = None + self._surface_grazing_cutoff = None + self._surface_grazing_ratio = None self._survival_biasing = None self._free_gas_threshold = None @@ -710,6 +720,27 @@ class Settings: cv.check_greater_than('random number generator stride', stride, 0) self._stride = stride + @property + def surface_grazing_cutoff(self) -> float: + return self._surface_grazing_cutoff + + @surface_grazing_cutoff.setter + def surface_grazing_cutoff(self, surface_grazing_cutoff: float): + cv.check_type('surface grazing cutoff', surface_grazing_cutoff, float) + cv.check_greater_than('surface grazing cutoff', surface_grazing_cutoff, 0.0) + cv.check_less_than('surface grazing cutoff', surface_grazing_cutoff, 1.0) + self._surface_grazing_cutoff = surface_grazing_cutoff + + @property + def surface_grazing_ratio(self) -> float: + return self._surface_grazing_ratio + + @surface_grazing_ratio.setter + def surface_grazing_ratio(self, surface_grazing_ratio: float): + cv.check_type('surface grazing ratio', surface_grazing_ratio, float) + cv.check_greater_than('surface grazing ratio', surface_grazing_ratio, 0.0) + self._surface_grazing_ratio = surface_grazing_ratio + @property def survival_biasing(self) -> bool: return self._survival_biasing @@ -1625,6 +1656,16 @@ class Settings: element = ET.SubElement(root, "stride") element.text = str(self._stride) + def _create_surface_grazing_cutoff_subelement(self, root): + if self._surface_grazing_cutoff is not None: + element = ET.SubElement(root, "surface_grazing_cutoff") + element.text = str(self._surface_grazing_cutoff) + + def _create_surface_grazing_ratio_subelement(self, root): + if self._surface_grazing_ratio is not None: + element = ET.SubElement(root, "surface_grazing_ratio") + element.text = str(self._surface_grazing_ratio) + def _create_survival_biasing_subelement(self, root): if self._survival_biasing is not None: element = ET.SubElement(root, "survival_biasing") @@ -2128,6 +2169,16 @@ class Settings: if text is not None: self.stride = int(text) + def _surface_grazing_cutoff_from_xml_element(self, root): + text = get_text(root, 'surface_grazing_cutoff') + if text is not None: + self.surface_grazing_cutoff = float(text) + + def _surface_grazing_ratio_from_xml_element(self, root): + text = get_text(root, 'surface_grazing_ratio') + if text is not None: + self.surface_grazing_ratio = float(text) + def _survival_biasing_from_xml_element(self, root): text = get_text(root, 'survival_biasing') if text is not None: @@ -2435,6 +2486,8 @@ class Settings: self._create_ptables_subelement(element) self._create_seed_subelement(element) self._create_stride_subelement(element) + self._create_surface_grazing_cutoff_subelement(element) + self._create_surface_grazing_ratio_subelement(element) self._create_survival_biasing_subelement(element) self._create_cutoff_subelement(element) self._create_entropy_mesh_subelement(element, mesh_memo) @@ -2549,6 +2602,8 @@ class Settings: settings._ptables_from_xml_element(elem) settings._seed_from_xml_element(elem) settings._stride_from_xml_element(elem) + settings._surface_grazing_cutoff_from_xml_element(elem) + settings._surface_grazing_ratio_from_xml_element(elem) settings._survival_biasing_from_xml_element(elem) settings._cutoff_from_xml_element(elem) settings._entropy_mesh_from_xml_element(elem, meshes) diff --git a/src/finalize.cpp b/src/finalize.cpp index 4ac6d09f32..e82e00b4cc 100644 --- a/src/finalize.cpp +++ b/src/finalize.cpp @@ -123,6 +123,8 @@ int openmc_finalize() settings::restart_run = false; settings::run_CE = true; settings::run_mode = RunMode::UNSET; + settings::surface_grazing_cutoff = 0.001; + settings::surface_grazing_ratio = 0.5; settings::solver_type = SolverType::MONTE_CARLO; settings::source_latest = false; settings::source_rejection_fraction = 0.05; diff --git a/src/particle.cpp b/src/particle.cpp index 8d0e5b3f06..0a06355982 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -302,6 +302,8 @@ void Particle::event_cross_surface() surface() = boundary().surface(); n_coord() = boundary().coord_level(); + const auto& surf {*model::surfaces[surface_index()].get()}; + if (boundary().lattice_translation()[0] != 0 || boundary().lattice_translation()[1] != 0 || boundary().lattice_translation()[2] != 0) { @@ -312,15 +314,14 @@ void Particle::event_cross_surface() event() = TallyEvent::LATTICE; } else { // Particle crosses surface - const auto& surf {model::surfaces[surface_index()].get()}; // If BC, add particle to surface source before crossing surface - if (surf->surf_source_ && surf->bc_) { - add_surf_source_to_bank(*this, *surf); + if (surf.surf_source_ && surf.bc_) { + add_surf_source_to_bank(*this, surf); } - this->cross_surface(*surf); + this->cross_surface(surf); // If no BC, add particle to surface source after crossing surface - if (surf->surf_source_ && !surf->bc_) { - add_surf_source_to_bank(*this, *surf); + if (surf.surf_source_ && !surf.bc_) { + add_surf_source_to_bank(*this, surf); } if (settings::weight_window_checkpoint_surface) { apply_weight_windows(*this); @@ -329,7 +330,7 @@ void Particle::event_cross_surface() } // Score cell to cell partial currents if (!model::active_surface_tallies.empty()) { - score_surface_tally(*this, model::active_surface_tallies); + score_surface_tally(*this, model::active_surface_tallies, surf); } } @@ -346,7 +347,7 @@ void Particle::event_collide() // pre-collision direction to figure out what mesh surfaces were crossed if (!model::active_meshsurf_tallies.empty()) - score_surface_tally(*this, model::active_meshsurf_tallies); + score_meshsurface_tally(*this, model::active_meshsurf_tallies); // Clear surface component surface() = SURFACE_NONE; @@ -648,7 +649,7 @@ void Particle::cross_vacuum_bc(const Surface& surf) // physically moving the particle forward slightly r() += TINY_BIT * u(); - score_surface_tally(*this, model::active_meshsurf_tallies); + score_meshsurface_tally(*this, model::active_meshsurf_tallies); } // Score to global leakage tally @@ -680,13 +681,13 @@ void Particle::cross_reflective_bc(const Surface& surf, Direction new_u) // with a mesh boundary if (!model::active_surface_tallies.empty()) { - score_surface_tally(*this, model::active_surface_tallies); + score_surface_tally(*this, model::active_surface_tallies, surf); } if (!model::active_meshsurf_tallies.empty()) { Position r {this->r()}; this->r() -= TINY_BIT * u(); - score_surface_tally(*this, model::active_meshsurf_tallies); + score_meshsurface_tally(*this, model::active_meshsurf_tallies); this->r() = r; } @@ -736,7 +737,7 @@ void Particle::cross_periodic_bc( if (!model::active_meshsurf_tallies.empty()) { Position r {this->r()}; this->r() -= TINY_BIT * u(); - score_surface_tally(*this, model::active_meshsurf_tallies); + score_meshsurface_tally(*this, model::active_meshsurf_tallies); this->r() = r; } diff --git a/src/settings.cpp b/src/settings.cpp index ee8edd4a52..6b159f6e90 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -136,6 +136,8 @@ int64_t ssw_max_particles; int64_t ssw_max_files; int64_t ssw_cell_id {C_NONE}; SSWCellType ssw_cell_type {SSWCellType::None}; +double surface_grazing_cutoff {0.001}; +double surface_grazing_ratio {0.5}; TemperatureMethod temperature_method {TemperatureMethod::NEAREST}; double temperature_tolerance {10.0}; double temperature_default {293.6}; @@ -678,6 +680,14 @@ void read_settings_xml(pugi::xml_node root) free_gas_threshold = std::stod(get_node_value(root, "free_gas_threshold")); } + // Surface grazing + if (check_for_node(root, "surface_grazing_cutoff")) + surface_grazing_cutoff = + std::stod(get_node_value(root, "surface_grazing_cutoff")); + if (check_for_node(root, "surface_grazing_ratio")) + surface_grazing_ratio = + std::stod(get_node_value(root, "surface_grazing_ratio")); + // Survival biasing if (check_for_node(root, "survival_biasing")) { survival_biasing = get_node_value_bool(root, "survival_biasing"); diff --git a/src/tallies/filter_surface.cpp b/src/tallies/filter_surface.cpp index 82f3d71789..4a636f1aa3 100644 --- a/src/tallies/filter_surface.cpp +++ b/src/tallies/filter_surface.cpp @@ -52,11 +52,7 @@ void SurfaceFilter::get_all_bins( auto search = map_.find(p.surface_index()); if (search != map_.end()) { match.bins_.push_back(search->second); - if (p.surface() < 0) { - match.weights_.push_back(-1.0); - } else { - match.weights_.push_back(1.0); - } + match.weights_.push_back(1.0); } } diff --git a/src/tallies/tally.cpp b/src/tallies/tally.cpp index 13225adcaf..3fe48c1b02 100644 --- a/src/tallies/tally.cpp +++ b/src/tallies/tally.cpp @@ -540,6 +540,8 @@ void Tally::set_scores(const vector& scores) bool legendre_present = false; bool cell_present = false; bool cellfrom_present = false; + bool material_present = false; + bool materialfrom_present = false; bool surface_present = false; bool meshsurface_present = false; bool non_cell_energy_present = false; @@ -556,12 +558,21 @@ void Tally::set_scores(const vector& scores) cellfrom_present = true; } else if (filt->type() == FilterType::CELL) { cell_present = true; + } else if (filt->type() == FilterType::MATERIALFROM) { + materialfrom_present = true; + } else if (filt->type() == FilterType::MATERIAL) { + material_present = true; } else if (filt->type() == FilterType::SURFACE) { surface_present = true; } else if (filt->type() == FilterType::MESH_SURFACE) { meshsurface_present = true; } } + bool surface_types_present = + (surface_present || cellfrom_present || materialfrom_present); + bool non_meshsurface_types_present = + (surface_present || cell_present || cellfrom_present || material_present || + materialfrom_present); // Iterate over the given scores. for (auto score_str : scores) { @@ -583,6 +594,12 @@ void Tally::set_scores(const vector& scores) fatal_error("Cannot tally flux for an individual nuclide."); if (energyout_present) fatal_error("Cannot tally flux with an outgoing energy filter."); + if (surface_types_present) { + if (meshsurface_present) + fatal_error("OpenMC does not support mesh surface fluxes yet"); + type_ = TallyType::SURFACE; + estimator_ = TallyEstimator::ANALOG; + } break; case SCORE_TOTAL: @@ -615,16 +632,14 @@ void Tally::set_scores(const vector& scores) case SCORE_CURRENT: // Check which type of current is desired: mesh or surface currents. - if (surface_present || cell_present || cellfrom_present) { - if (meshsurface_present) + if (meshsurface_present) { + if (non_meshsurface_types_present) fatal_error("Cannot tally mesh surface currents in the same tally as " "normal surface currents"); - type_ = TallyType::SURFACE; - estimator_ = TallyEstimator::ANALOG; - } else if (meshsurface_present) { type_ = TallyType::MESH_SURFACE; } else { - fatal_error("Cannot tally currents without surface type filters"); + type_ = TallyType::SURFACE; + estimator_ = TallyEstimator::ANALOG; } break; @@ -681,15 +696,20 @@ void Tally::set_scores(const vector& scores) "in multi-group mode"); } - // Make sure current scores are not mixed in with volumetric scores. - if (type_ == TallyType::SURFACE || type_ == TallyType::MESH_SURFACE) { - if (scores_.size() != 1) - fatal_error("Cannot tally other scores in the same tally as surface " - "currents."); + // Make sure mesh surface tallies contain only current score. + if (meshsurface_present) { + if ((scores_[0] != SCORE_CURRENT) || (scores_.size() > 1)) + fatal_error("Cannot tally score other than 'current' when using a " + "mesh-surface filter."); + } + + // Make sure surface tallies contain only surface type scores score. + if (type_ == TallyType::SURFACE) { + for (auto sc : scores_) + if ((sc != SCORE_CURRENT) && (sc != SCORE_FLUX)) + fatal_error("Cannot tally scores other than 'current' or 'flux' " + "when using surface filters."); } - if ((surface_present || meshsurface_present) && scores_[0] != SCORE_CURRENT) - fatal_error("Cannot tally score other than 'current' when using a surface " - "or mesh-surface filter."); } void Tally::set_nuclides(pugi::xml_node node) diff --git a/src/tallies/tally_scoring.cpp b/src/tallies/tally_scoring.cpp index cc7b16f44d..4210b034a9 100644 --- a/src/tallies/tally_scoring.cpp +++ b/src/tallies/tally_scoring.cpp @@ -14,6 +14,7 @@ #include "openmc/settings.h" #include "openmc/simulation.h" #include "openmc/string_utils.h" +#include "openmc/surface.h" #include "openmc/tallies/derivative.h" #include "openmc/tallies/filter.h" #include "openmc/tallies/filter_cell.h" @@ -2610,7 +2611,7 @@ void score_collision_tally(Particle& p) match.bins_present_ = false; } -void score_surface_tally(Particle& p, const vector& tallies) +void score_meshsurface_tally(Particle& p, const vector& tallies) { double current = p.wgt_last(); @@ -2654,6 +2655,69 @@ void score_surface_tally(Particle& p, const vector& tallies) match.bins_present_ = false; } +void score_surface_tally( + Particle& p, const vector& tallies, const Surface& surf) +{ + double wgt = p.wgt_last(); + + // Sign for net current: +1 if crossing outward (in direction of normal), + // -1 if crossing inward + double current_sign = (p.surface() > 0) ? 1.0 : -1.0; + + // Determine absolute cosine of angle between particle direction and surface + // normal, needed for the surface-crossing flux estimator. + auto n = surf.normal(p.r()); + n /= n.norm(); + double abs_mu = std::min(std::abs(p.u().dot(n)), 1.0); + if (abs_mu < settings::surface_grazing_cutoff) + abs_mu = settings::surface_grazing_ratio * settings::surface_grazing_cutoff; + + for (auto i_tally : tallies) { + auto& tally {*model::tallies[i_tally]}; + + // Initialize an iterator over valid filter bin combinations. If there are + // no valid combinations, use a continue statement to ensure we skip the + // assume_separate break below. + auto filter_iter = FilterBinIter(tally, p); + auto end = FilterBinIter(tally, true, &p.filter_matches()); + if (filter_iter == end) + continue; + + // Loop over filter bins. + for (; filter_iter != end; ++filter_iter) { + auto filter_index = filter_iter.index_; + auto filter_weight = filter_iter.weight_; + + // Loop over scores. + for (auto score_index = 0; score_index < tally.scores_.size(); + ++score_index) { + auto score_bin = tally.scores_[score_index]; + double score; + if (score_bin == SCORE_CURRENT) { + // Net current: weight carries the sign of the crossing direction. + score = wgt * current_sign; + } else { + // SCORE_FLUX: surface-crossing estimator phi_S = sum(w / |mu|). + score = wgt / abs_mu; + } +#pragma omp atomic + tally.results_(filter_index, score_index, TallyResult::VALUE) += + score * filter_weight; + } + } + // If the user has specified that we can assume all tallies are spatially + // separate, this implies that once a tally has been scored to, we needn't + // check the others. This cuts down on overhead when there are many + // tallies specified + if (settings::assume_separate) + break; + } + + // Reset all the filter matches for the next tally event. + for (auto& match : p.filter_matches()) + match.bins_present_ = false; +} + void score_pulse_height_tally(Particle& p, const vector& tallies) { // The pulse height tally in OpenMC hijacks the logic of CellFilter and diff --git a/tests/regression_tests/filter_musurface/inputs_true.dat b/tests/regression_tests/filter_musurface/inputs_true.dat index 6db8543c2d..b457f5028e 100644 --- a/tests/regression_tests/filter_musurface/inputs_true.dat +++ b/tests/regression_tests/filter_musurface/inputs_true.dat @@ -31,7 +31,7 @@ 1 2 - current + current flux diff --git a/tests/regression_tests/filter_musurface/results_true.dat b/tests/regression_tests/filter_musurface/results_true.dat index 4cdd7dbf50..657c141a05 100644 --- a/tests/regression_tests/filter_musurface/results_true.dat +++ b/tests/regression_tests/filter_musurface/results_true.dat @@ -5,7 +5,15 @@ tally 1: 0.000000E+00 0.000000E+00 0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 9.230000E-01 1.791510E-01 +3.294304E+00 +2.314403E+00 3.869000E+00 3.002523E+00 +5.154125E+00 +5.314334E+00 diff --git a/tests/regression_tests/filter_musurface/test.py b/tests/regression_tests/filter_musurface/test.py index f2ec96b495..bfc12a47cd 100644 --- a/tests/regression_tests/filter_musurface/test.py +++ b/tests/regression_tests/filter_musurface/test.py @@ -1,6 +1,3 @@ -import numpy as np -from math import pi - import openmc import pytest @@ -32,7 +29,7 @@ def model(): mu_filter = openmc.MuSurfaceFilter([-1.0, -0.5, 0.0, 0.5, 1.0]) tally = openmc.Tally() tally.filters = [surf_filter, mu_filter] - tally.scores = ['current'] + tally.scores = ['current', 'flux'] model.tallies.append(tally) return model diff --git a/tests/regression_tests/surface_tally/results_true.dat b/tests/regression_tests/surface_tally/results_true.dat index 70d5cad2c6..e50102a887 100644 --- a/tests/regression_tests/surface_tally/results_true.dat +++ b/tests/regression_tests/surface_tally/results_true.dat @@ -15,14 +15,14 @@ mean,std. dev. 5.5000000e-03,1.0979779e-03 1.2500000e-02,1.5438048e-03 4.4700000e-02,1.9035055e-03 -7.3000000e-03,9.8938814e-04 -3.6600000e-02,2.5086517e-03 -4.1600000e-02,2.4864075e-03 -4.2380000e-01,9.6087923e-03 -1.0000000e-04,1.0000000e-04 -1.5000000e-03,4.5338235e-04 -3.0000000e-04,1.5275252e-04 -1.7300000e-02,1.4609738e-03 +-7.3000000e-03,9.8938814e-04 +-3.6600000e-02,2.5086517e-03 +-4.1600000e-02,2.4864075e-03 +-4.2380000e-01,9.6087923e-03 +-1.0000000e-04,1.0000000e-04 +-1.5000000e-03,4.5338235e-04 +-3.0000000e-04,1.5275252e-04 +-1.7300000e-02,1.4609738e-03 -7.3000000e-03,9.8938814e-04 -3.6600000e-02,2.5086517e-03 -4.1600000e-02,2.4864075e-03 diff --git a/tests/regression_tests/surface_tally/test.py b/tests/regression_tests/surface_tally/test.py index e496ac0f65..aa03a8aa2d 100644 --- a/tests/regression_tests/surface_tally/test.py +++ b/tests/regression_tests/surface_tally/test.py @@ -169,7 +169,6 @@ class SurfaceTallyTestHarness(PyAPITestHarness): # Extract the relevant data as a CSV string. cols = ('mean', 'std. dev.') return df.to_csv(None, columns=cols, index=False, float_format='%.7e') - return outstr def test_surface_tally(): diff --git a/tests/unit_tests/test_settings.py b/tests/unit_tests/test_settings.py index fe618fd2d6..9f693802ca 100644 --- a/tests/unit_tests/test_settings.py +++ b/tests/unit_tests/test_settings.py @@ -21,6 +21,8 @@ def test_export_to_xml(run_in_tmpdir): s.statepoint = {'batches': [50, 150, 500, 1000]} s.surf_source_read = {'path': 'surface_source_1.h5'} s.surf_source_write = {'surface_ids': [2], 'max_particles': 200} + s.surface_grazing_ratio = 0.7 + s.surface_grazing_cutoff = 0.1 s.confidence_intervals = True s.ptables = True s.plot_seed = 100 @@ -107,6 +109,8 @@ def test_export_to_xml(run_in_tmpdir): assert s.statepoint == {'batches': [50, 150, 500, 1000]} assert s.surf_source_read['path'].name == 'surface_source_1.h5' assert s.surf_source_write == {'surface_ids': [2], 'max_particles': 200} + assert s.surface_grazing_ratio == 0.7 + assert s.surface_grazing_cutoff == 0.1 assert s.confidence_intervals assert s.ptables assert s.plot_seed == 100 diff --git a/tests/unit_tests/test_surface_flux.py b/tests/unit_tests/test_surface_flux.py new file mode 100644 index 0000000000..4e067ffe23 --- /dev/null +++ b/tests/unit_tests/test_surface_flux.py @@ -0,0 +1,120 @@ +"""Tests for surface flux tallying via flux score + SurfaceFilter.""" + +import math +import pytest + +import openmc + + +@pytest.fixture +def two_cell_model(): + """Simple two-cell slab model with a monodirectional fixed source. + + Cell1 occupies x in [-10, 0], cell2 x in [0, 10]. The source fires all + particles from (-5, 0, 0) in the +x direction with weight 1. Every + particle therefore crosses the surface at x=0 from cell1 into cell2 at + mu = 1 (normal incidence). + """ + openmc.reset_auto_ids() + model = openmc.Model() + + xmin = openmc.XPlane(-10.0, boundary_type="vacuum") + xmid = openmc.XPlane(0.0) + xmax = openmc.XPlane(10.0, boundary_type="vacuum") + ymin = openmc.YPlane(-10.0, boundary_type="vacuum") + ymax = openmc.YPlane(10.0, boundary_type="vacuum") + zmin = openmc.ZPlane(-10.0, boundary_type="vacuum") + zmax = openmc.ZPlane(10.0, boundary_type="vacuum") + + cell1 = openmc.Cell(region=+xmin & -xmid & +ymin & -ymax & +zmin & -zmax) + cell2 = openmc.Cell(region=+xmid & -xmax & +ymin & -ymax & +zmin & -zmax) + model.geometry = openmc.Geometry([cell1, cell2]) + + src = openmc.IndependentSource() + src.space = openmc.stats.Point((-5.0, 0.0, 0.0)) + src.angle = openmc.stats.Monodirectional((1.0, 0.0, 0.0)) + + model.settings.run_mode = 'fixed source' + model.settings.batches = 1 + model.settings.particles = 100 + model.settings.source = src + + return model, xmid, cell1, cell2 + + +def test_surface_filter_flux_normal_incidence(two_cell_model, run_in_tmpdir): + """SurfaceFilter + flux at mu=1 gives w/|mu| = 1.0 per source particle.""" + model, xmid, *_ = two_cell_model + + surf_filter = openmc.SurfaceFilter([xmid]) + flux_tally = openmc.Tally() + flux_tally.filters = [surf_filter] + flux_tally.scores = ['flux'] + model.tallies = [flux_tally] + + model.run(apply_tally_results=True) + flux_mean = flux_tally.mean.flat[0] + + # Every particle crosses at mu=1 with weight 1, so flux = 1.0 + assert flux_mean == pytest.approx(1.0, rel=1e-8) + + +def test_surface_filter_current_outward(two_cell_model, run_in_tmpdir): + """SurfaceFilter + current gives +1.0 for purely outward crossings.""" + model, xmid, *_ = two_cell_model + + surf_filter = openmc.SurfaceFilter([xmid]) + current_tally = openmc.Tally() + current_tally.filters = [surf_filter] + current_tally.scores = ['current'] + model.tallies = [current_tally] + + model.run(apply_tally_results=True) + current_mean = current_tally.mean.flat[0] + + # All crossings are outward → net current = +1.0 + assert current_mean == pytest.approx(1.0) + + +def test_surface_filter_flux_angled(two_cell_model, run_in_tmpdir): + """Surface flux at 60-degree incidence gives w/|mu| = 2.0.""" + model, xmid, *_ = two_cell_model + + # Modify source to use 60-degree angle from normal: mu = cos(60°) = 0.5 + mu = ux = 0.5 + uy = math.sqrt(1.0 - ux**2) + model.settings.source[0].angle = openmc.stats.Monodirectional((ux, uy, 0.0)) + + surf_filter = openmc.SurfaceFilter([xmid]) + flux_tally = openmc.Tally() + flux_tally.filters = [surf_filter] + flux_tally.scores = ['flux'] + model.tallies = [flux_tally] + + model.run(apply_tally_results=True) + flux_mean = flux_tally.mean.flat[0] + + # flux = w/|mu| = 1/0.5 = 2.0 + assert flux_mean == pytest.approx(1.0 / mu) + + +def test_cellfrom_filter_flux_directional(two_cell_model, run_in_tmpdir): + """SurfaceFilter + CellFromFilter + flux scores only the correct direction.""" + model, xmid, cell1, cell2 = two_cell_model + + surf_filter = openmc.SurfaceFilter([xmid]) + cellfrom_filter = openmc.CellFromFilter([cell1, cell2]) + + tally = openmc.Tally() + tally.filters = [surf_filter, cellfrom_filter] + tally.scores = ['flux'] + + model.tallies = [tally] + model.run(apply_tally_results=True) + mean_from1 = tally.mean.flat[0] + mean_from2 = tally.mean.flat[1] + + # All particles cross xmid from cell1 at mu=1 → flux = 1.0 + assert mean_from1 == pytest.approx(1.0) + # No particles cross xmid from cell2 → flux = 0 + assert mean_from2 == pytest.approx(0.0) From 0ab46dfa350eeb728125446e6b33af7bf497a9be Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Wed, 4 Mar 2026 14:56:49 +0200 Subject: [PATCH 32/38] Fix cell data parsing (#3848) --- openmc/cell.py | 11 ++--------- tests/unit_tests/test_model.py | 8 ++++---- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/openmc/cell.py b/openmc/cell.py index 945dff0dbd..499cf95042 100644 --- a/openmc/cell.py +++ b/openmc/cell.py @@ -747,15 +747,6 @@ class Cell(IDManagerMixin): c.region = Region.from_expression(region, surfaces) # Check for other attributes - temperature = get_elem_list(elem, 'temperature', float) - if temperature is not None: - if len(temperature) > 1: - c.temperature = temperature - else: - c.temperature = temperature[0] - density = get_elem_list(elem, 'density', float) - if density is not None: - c.density = density if len(density) > 1 else density[0] v = get_text(elem, 'volume') if v is not None: c.volume = float(v) @@ -764,6 +755,8 @@ class Cell(IDManagerMixin): if values is not None: if key == 'rotation' and len(values) == 9: values = np.array(values).reshape(3, 3) + elif len(values) == 1: + values = values[0] setattr(c, key, values) # Add this cell to appropriate universe diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 3846ba4fb8..9234b2d272 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -265,8 +265,8 @@ def test_import_properties(run_in_tmpdir, mpi_intracomm): # Check to see that values are assigned to the C and python representations # First python cell = model.geometry.get_all_cells()[1] - assert cell.temperature == [600.0] - assert cell.density == [pytest.approx(10.0, 1e-5)] + assert cell.temperature == 600.0 + assert cell.density == pytest.approx(10.0, 1e-5) assert cell.fill.get_mass_density() == pytest.approx(5.0) # Now C assert openmc.lib.cells[1].get_temperature() == 600. @@ -286,8 +286,8 @@ def test_import_properties(run_in_tmpdir, mpi_intracomm): 'with_properties/settings.xml' ) cell = model_with_properties.geometry.get_all_cells()[1] - assert cell.temperature == [600.0] - assert cell.density == [pytest.approx(10.0, 1e-5)] + assert cell.temperature == 600.0 + assert cell.density == pytest.approx(10.0, 1e-5) assert cell.fill.get_mass_density() == pytest.approx(5.0) From 2bd06660c570bbebdff2fa57e77a135753e86aa9 Mon Sep 17 00:00:00 2001 From: Ethan Peterson Date: Wed, 4 Mar 2026 15:36:43 -0500 Subject: [PATCH 33/38] Parallelize sampling external sources and threadsafe rejection counters (#3830) Co-authored-by: Paul Romano --- include/openmc/source.h | 12 +++++++ openmc/lib/core.py | 47 +++++++++++++++++-------- openmc/model/model.py | 15 +++++--- src/simulation.cpp | 1 + src/source.cpp | 68 ++++++++++++++++++++++++++---------- tests/unit_tests/test_lib.py | 8 +++++ 6 files changed, 114 insertions(+), 37 deletions(-) diff --git a/include/openmc/source.h b/include/openmc/source.h index 2f32aa2a05..1ef7eba2b2 100644 --- a/include/openmc/source.h +++ b/include/openmc/source.h @@ -4,6 +4,7 @@ #ifndef OPENMC_SOURCE_H #define OPENMC_SOURCE_H +#include #include #include @@ -25,10 +26,18 @@ namespace openmc { // source_rejection_fraction constexpr int EXTSRC_REJECT_THRESHOLD {10000}; +// Maximum number of source rejections allowed while sampling a single site +constexpr int64_t MAX_SOURCE_REJECTIONS_PER_SAMPLE {1'000'000}; + //============================================================================== // Global variables //============================================================================== +// Cumulative counters for source rejection diagnostics. These are atomic to +// allow thread-safe concurrent sampling of external sources. +extern std::atomic source_n_accept; +extern std::atomic source_n_reject; + class Source; namespace model { @@ -265,6 +274,9 @@ SourceSite sample_external_source(uint64_t* seed); void free_memory_source(); +//! Reset cumulative source rejection counters +void reset_source_rejection_counters(); + } // namespace openmc #endif // OPENMC_SOURCE_H diff --git a/openmc/lib/core.py b/openmc/lib/core.py index 2764ddff1a..8e5cb56dc7 100644 --- a/openmc/lib/core.py +++ b/openmc/lib/core.py @@ -33,7 +33,6 @@ class _SourceSite(Structure): ('parent_id', c_int64), ('progeny_id', c_int64)] - # Define input type for numpy arrays that will be passed into C++ functions # Must be an int or double array, with single dimension that is contiguous _array_1d_int = np.ctypeslib.ndpointer(dtype=np.int32, ndim=1, @@ -494,8 +493,9 @@ def run_random_ray(output=True): def sample_external_source( n_samples: int = 1000, - prn_seed: int | None = None -) -> openmc.ParticleList: + prn_seed: int | None = None, + as_array: bool = False +) -> openmc.ParticleList | np.ndarray: """Sample external source and return source particles. .. versionadded:: 0.13.1 @@ -507,11 +507,20 @@ def sample_external_source( prn_seed : int Pseudorandom number generator (PRNG) seed; if None, one will be generated randomly. + as_array : bool + If True, return a numpy structured array instead of a + :class:`~openmc.ParticleList`. The array has fields ``'r'`` (float64, + shape 3), ``'u'`` (float64, shape 3), ``'E'`` (float64), ``'time'`` + (float64), ``'wgt'`` (float64), ``'delayed_group'`` (int32), + ``'surf_id'`` (int32), and ``'particle'`` (int32). This avoids the + overhead of constructing individual :class:`~openmc.SourceParticle` + objects and is substantially faster for large sample counts. Returns ------- - openmc.ParticleList - List of sampled source particles + openmc.ParticleList or numpy.ndarray + List of sampled source particles, or a structured array when + *as_array* is True. """ if n_samples <= 0: @@ -519,18 +528,28 @@ def sample_external_source( if prn_seed is None: prn_seed = getrandbits(63) - # Call into C API to sample source - sites_array = (_SourceSite * n_samples)() - _dll.openmc_sample_external_source(c_size_t(n_samples), c_uint64(prn_seed), sites_array) + # Pre-allocate output array and sample all particles in a single C call + result = np.empty(n_samples, dtype=_SourceSite) + sites_array = (_SourceSite * n_samples).from_buffer(result) + _dll.openmc_sample_external_source( + c_size_t(n_samples), + c_uint64(prn_seed), + sites_array, + ) - # Convert to list of SourceParticle and return - return openmc.ParticleList([openmc.SourceParticle( - r=site.r, u=site.u, E=site.E, time=site.time, wgt=site.wgt, - delayed_group=site.delayed_group, surf_id=site.surf_id, - particle=openmc.ParticleType(site.particle) + if as_array: + return result + + particles = [ + openmc.SourceParticle( + r=site.r, u=site.u, E=site.E, time=site.time, + wgt=site.wgt, delayed_group=site.delayed_group, + surf_id=site.surf_id, + particle=openmc.ParticleType(site.particle), ) for site in sites_array - ]) + ] + return openmc.ParticleList(particles) def simulation_init(): diff --git a/openmc/model/model.py b/openmc/model/model.py index 0920b79e48..67a8495ee7 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -1294,8 +1294,9 @@ class Model: self, n_samples: int = 1000, prn_seed: int | None = None, + as_array: bool = False, **init_kwargs - ) -> openmc.ParticleList: + ) -> openmc.ParticleList | np.ndarray: """Sample external source and return source particles. .. versionadded:: 0.15.1 @@ -1307,13 +1308,17 @@ class Model: prn_seed : int Pseudorandom number generator (PRNG) seed; if None, one will be generated randomly. + as_array : bool + If True, return a numpy structured array instead of a + :class:`~openmc.ParticleList`. **init_kwargs Keyword arguments passed to :func:`openmc.lib.init` Returns ------- - openmc.ParticleList - List of samples source particles + openmc.ParticleList or numpy.ndarray + List of sampled source particles, or a structured array when + *as_array* is True. """ import openmc.lib @@ -1324,7 +1329,7 @@ class Model: with openmc.lib.TemporarySession(self, **init_kwargs): return openmc.lib.sample_external_source( - n_samples=n_samples, prn_seed=prn_seed + n_samples=n_samples, prn_seed=prn_seed, as_array=as_array ) def apply_tally_results(self, statepoint: PathLike | openmc.StatePoint): @@ -2588,7 +2593,7 @@ class Model: # This mode doesn't require # valid transport settings like particles/batches original_run_mode = self.settings.run_mode - self.settings.run_mode = 'volume' + self.settings.run_mode = 'volume' self.init_lib(directory=tmpdir) self.sync_dagmc_universes() self.finalize_lib() diff --git a/src/simulation.cpp b/src/simulation.cpp index d0d6f037f9..4fad196a60 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -122,6 +122,7 @@ int openmc_simulation_init() simulation::ssw_current_file = 1; simulation::k_generation.clear(); simulation::entropy.clear(); + reset_source_rejection_counters(); openmc_reset(); // If this is a restart run, load the state point data and binary source diff --git a/src/source.cpp b/src/source.cpp index 5300a685f7..f0b8f48edd 100644 --- a/src/source.cpp +++ b/src/source.cpp @@ -37,6 +37,9 @@ namespace openmc { +std::atomic source_n_accept {0}; +std::atomic source_n_reject {0}; + namespace { void validate_particle_type(ParticleType type, const std::string& context) @@ -191,9 +194,8 @@ void check_rejection_fraction(int64_t n_reject, int64_t n_accept) SourceSite Source::sample_with_constraints(uint64_t* seed) const { bool accepted = false; - static int64_t n_reject = 0; - static int64_t n_accept = 0; - SourceSite site; + int64_t n_local_reject = 0; + SourceSite site {}; while (!accepted) { // Sample a source site without considering constraints yet @@ -207,9 +209,13 @@ SourceSite Source::sample_with_constraints(uint64_t* seed) const satisfies_energy_constraints(site.E) && satisfies_time_constraints(site.time); if (!accepted) { - // Increment number of rejections and check against minimum fraction - ++n_reject; - check_rejection_fraction(n_reject, n_accept); + ++n_local_reject; + + // Check per-particle rejection limit + if (n_local_reject >= MAX_SOURCE_REJECTIONS_PER_SAMPLE) { + fatal_error("Exceeded maximum number of source rejections per " + "sample. Please check your source definition."); + } // For the "kill" strategy, accept particle but set weight to 0 so that // it is terminated immediately @@ -221,8 +227,13 @@ SourceSite Source::sample_with_constraints(uint64_t* seed) const } } - // Increment number of accepted samples - ++n_accept; + // Flush local rejection count, update accept counter, and check overall + // rejection fraction + if (n_local_reject > 0) { + source_n_reject += n_local_reject; + } + ++source_n_accept; + check_rejection_fraction(source_n_reject, source_n_accept); return site; } @@ -361,15 +372,14 @@ IndependentSource::IndependentSource(pugi::xml_node node) : Source(node) SourceSite IndependentSource::sample(uint64_t* seed) const { - SourceSite site; + SourceSite site {}; site.particle = particle_; double r_wgt = 1.0; double E_wgt = 1.0; // Repeat sampling source location until a good site has been accepted bool accepted = false; - static int64_t n_reject = 0; - static int64_t n_accept = 0; + int64_t n_local_reject = 0; while (!accepted) { @@ -383,8 +393,11 @@ SourceSite IndependentSource::sample(uint64_t* seed) const // Check for rejection if (!accepted) { - ++n_reject; - check_rejection_fraction(n_reject, n_accept); + ++n_local_reject; + if (n_local_reject >= MAX_SOURCE_REJECTIONS_PER_SAMPLE) { + fatal_error("Exceeded maximum number of source rejections per " + "sample. Please check your source definition."); + } } } @@ -419,8 +432,11 @@ SourceSite IndependentSource::sample(uint64_t* seed) const (satisfies_energy_constraints(site.E))) break; - n_reject++; - check_rejection_fraction(n_reject, n_accept); + ++n_local_reject; + if (n_local_reject >= MAX_SOURCE_REJECTIONS_PER_SAMPLE) { + fatal_error("Exceeded maximum number of source rejections per " + "sample. Please check your source definition."); + } } // Sample particle creation time @@ -430,8 +446,10 @@ SourceSite IndependentSource::sample(uint64_t* seed) const site.wgt *= (E_wgt * time_wgt); } - // Increment number of accepted samples - ++n_accept; + // Flush local rejection count into global counter + if (n_local_reject > 0) { + source_n_reject += n_local_reject; + } return site; } @@ -692,6 +710,13 @@ SourceSite sample_external_source(uint64_t* seed) void free_memory_source() { model::external_sources.clear(); + reset_source_rejection_counters(); +} + +void reset_source_rejection_counters() +{ + source_n_accept = 0; + source_n_reject = 0; } //============================================================================== @@ -712,8 +737,15 @@ extern "C" int openmc_sample_external_source( } auto sites_array = static_cast(sites); + + // Derive independent per-particle seeds from the base seed so that + // each iteration has its own RNG state for thread-safe parallel sampling. + uint64_t base_seed = *seed; + +#pragma omp parallel for schedule(static) for (size_t i = 0; i < n; ++i) { - sites_array[i] = sample_external_source(seed); + uint64_t particle_seed = init_seed(base_seed + i, STREAM_SOURCE); + sites_array[i] = sample_external_source(&particle_seed); } return 0; } diff --git a/tests/unit_tests/test_lib.py b/tests/unit_tests/test_lib.py index 8ef8d59271..51e648dcf9 100644 --- a/tests/unit_tests/test_lib.py +++ b/tests/unit_tests/test_lib.py @@ -1114,6 +1114,14 @@ def test_sample_external_source(run_in_tmpdir, mpi_intracomm): assert p1.time == p2.time assert p1.wgt == p2.wgt + # as_array should return a numpy structured array with matching values + arr = openmc.lib.sample_external_source(10, prn_seed=3, as_array=True) + assert isinstance(arr, np.ndarray) + assert len(arr) == 10 + for p, row in zip(particles, arr): + assert p.r == pytest.approx(row['r']) + assert p.E == pytest.approx(row['E']) + openmc.lib.finalize() # Make sure sampling works in volume calculation mode From dbfd6387b29d248bbfa6b812d00d27333940a7d5 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 4 Mar 2026 23:37:47 -0600 Subject: [PATCH 34/38] Fix two docstrings that should have r prefix (#3851) --- openmc/data/dose/mass_attenuation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openmc/data/dose/mass_attenuation.py b/openmc/data/dose/mass_attenuation.py index a6cbbee2bc..c4260480bf 100644 --- a/openmc/data/dose/mass_attenuation.py +++ b/openmc/data/dose/mass_attenuation.py @@ -62,7 +62,7 @@ _MUEN_TABLES = { def mass_energy_absorption_coefficient( material: str, data_source: str = "nist126" ) -> Tabulated1D: - """Return the mass energy-absorption coefficient as a function of energy. + r"""Return the mass energy-absorption coefficient as a function of energy. The mass energy-absorption coefficient, :math:`\mu_\text{en}/\rho`, is defined as the fraction of incident photon energy absorbed in a material per @@ -108,7 +108,7 @@ _MASS_ATTENUATION: dict[int, object] = {} def mass_attenuation_coefficient(element): - """Return the photon mass attenuation coefficient as a function of energy. + r"""Return the photon mass attenuation coefficient as a function of energy. The mass energy-absorption coefficient, :math:`\mu_\text{en}/\rho`, is defined as the fraction of incident photon energy absorbed in a material per From 533f09defd87c8e14d2d328d7edd90c47b10eec6 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Thu, 5 Mar 2026 17:20:54 -0600 Subject: [PATCH 35/38] Enable CMake "compile_commands.json" Output (#3854) Co-authored-by: John Tramm Co-authored-by: Claude Opus 4.6 Co-authored-by: Paul Romano --- CMakeLists.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 10867384b1..9b0e40d5a3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,6 +20,11 @@ set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) +# Generate compile_commands.json for clangd and other tools +if("${CMAKE_EXPORT_COMPILE_COMMANDS}" STREQUAL "") + set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +endif() + # Enable correct usage of CXX_EXTENSIONS if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.22) cmake_policy(SET CMP0128 NEW) From 6f72619729e7f2d0aa0f42f553ba2d24314ab7c1 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 5 Mar 2026 17:29:36 -0600 Subject: [PATCH 36/38] Fix include guard in output.h (#3856) --- include/openmc/output.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/include/openmc/output.h b/include/openmc/output.h index 7fcaa81ba4..0ad8b2fe50 100644 --- a/include/openmc/output.h +++ b/include/openmc/output.h @@ -62,7 +62,6 @@ void write_tallies(); void show_time(const char* label, double secs, int indent_level = 0); } // namespace openmc -#endif // OPENMC_OUTPUT_H ////////////////////////////////////// // Custom formatters @@ -89,3 +88,5 @@ struct formatter> { }; // namespace fmt } // namespace fmt + +#endif // OPENMC_OUTPUT_H From be4148ad0d9b54f1476117ed055b89e54a03fc9e Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Fri, 6 Mar 2026 03:07:42 +0200 Subject: [PATCH 37/38] Refactor Ray class into its own file (#3845) Co-authored-by: Paul Romano --- CMakeLists.txt | 1 + include/openmc/plot.h | 42 +---------- include/openmc/ray.h | 50 +++++++++++++ src/plot.cpp | 159 --------------------------------------- src/ray.cpp | 168 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 220 insertions(+), 200 deletions(-) create mode 100644 include/openmc/ray.h create mode 100644 src/ray.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 9b0e40d5a3..9fe133a22e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -412,6 +412,7 @@ list(APPEND libopenmc_SOURCES src/random_ray/linear_source_domain.cpp src/random_ray/moment_matrix.cpp src/random_ray/source_region.cpp + src/ray.cpp src/reaction.cpp src/reaction_product.cpp src/scattdata.cpp diff --git a/include/openmc/plot.h b/include/openmc/plot.h index aa37394531..6c00fc2f6b 100644 --- a/include/openmc/plot.h +++ b/include/openmc/plot.h @@ -17,6 +17,7 @@ #include "openmc/particle.h" #include "openmc/position.h" #include "openmc/random_lcg.h" +#include "openmc/ray.h" #include "openmc/xml_interface.h" namespace openmc { @@ -497,47 +498,6 @@ private: Position light_location_; }; -// Base class that implements ray tracing logic, not necessarily through -// defined regions of the geometry but also outside of it. -class Ray : public GeometryState { - -public: - // Initialize from location and direction - Ray(Position r, Direction u) { init_from_r_u(r, u); } - - // Initialize from known geometry state - Ray(const GeometryState& p) : GeometryState(p) {} - - // Called at every surface intersection within the model - virtual void on_intersection() = 0; - - /* - * Traces the ray through the geometry, calling on_intersection - * at every surface boundary. - */ - void trace(); - - // Stops the ray and exits tracing when called from on_intersection - void stop() { stop_ = true; } - - // Sets the dist_ variable - void compute_distance(); - -protected: - // Records how far the ray has traveled - double traversal_distance_ {0.0}; - -private: - // Max intersections before we assume ray tracing is caught in an infinite - // loop: - static const int MAX_INTERSECTIONS = 1000000; - - bool hit_something_ {false}; - bool stop_ {false}; - - unsigned event_counter_ {0}; -}; - class ProjectionRay : public Ray { public: ProjectionRay(Position r, Direction u, const WireframeRayTracePlot& plot, diff --git a/include/openmc/ray.h b/include/openmc/ray.h new file mode 100644 index 0000000000..62e86b0d90 --- /dev/null +++ b/include/openmc/ray.h @@ -0,0 +1,50 @@ +#ifndef OPENMC_RAY_H +#define OPENMC_RAY_H + +#include "openmc/particle_data.h" +#include "openmc/position.h" + +namespace openmc { + +// Base class that implements ray tracing logic, not necessarily through +// defined regions of the geometry but also outside of it. +class Ray : public GeometryState { + +public: + // Initialize from location and direction + Ray(Position r, Direction u) { init_from_r_u(r, u); } + + // Initialize from known geometry state + Ray(const GeometryState& p) : GeometryState(p) {} + + // Called at every surface intersection within the model + virtual void on_intersection() = 0; + + /* + * Traces the ray through the geometry, calling on_intersection + * at every surface boundary. + */ + void trace(); + + // Stops the ray and exits tracing when called from on_intersection + void stop() { stop_ = true; } + + // Sets the dist_ variable + void compute_distance(); + +protected: + // Records how far the ray has traveled + double traversal_distance_ {0.0}; + +private: + // Max intersections before we assume ray tracing is caught in an infinite + // loop: + static const int MAX_INTERSECTIONS = 1000000; + + bool stop_ {false}; + + unsigned event_counter_ {0}; +}; + +} // namespace openmc +#endif // OPENMC_RAY_H diff --git a/src/plot.cpp b/src/plot.cpp index b03c51e7e0..707d53dc2c 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -1649,165 +1649,6 @@ void SolidRayTracePlot::set_diffuse_fraction(pugi::xml_node node) } } -void Ray::compute_distance() -{ - boundary() = distance_to_boundary(*this); -} - -void Ray::trace() -{ - // To trace the ray from its origin all the way through the model, we have - // to proceed in two phases. In the first, the ray may or may not be found - // inside the model. If the ray is already in the model, phase one can be - // skipped. Otherwise, the ray has to be advanced to the boundary of the - // model where all the cells are defined. Importantly, this is assuming that - // the model is convex, which is a very reasonable assumption for any - // radiation transport model. - // - // After phase one is done, we can starting tracing from cell to cell within - // the model. This step can use neighbor lists to accelerate the ray tracing. - - bool inside_cell; - // Check for location if the particle is already known - if (lowest_coord().cell() == C_NONE) { - // The geometry position of the particle is either unknown or outside of the - // edge of the model. - if (lowest_coord().universe() == C_NONE) { - // Attempt to initialize the particle. We may have to - // enter a loop to move it up to the edge of the model. - inside_cell = exhaustive_find_cell(*this, settings::verbosity >= 10); - } else { - // It has been already calculated that the current position is outside of - // the edge of the model. - inside_cell = false; - } - } else { - // Availability of the cell means that the particle is located inside the - // edge. - inside_cell = true; - } - - // Advance to the boundary of the model - while (!inside_cell) { - advance_to_boundary_from_void(); - inside_cell = exhaustive_find_cell(*this, settings::verbosity >= 10); - - // If true this means no surface was intersected. See cell.cpp and search - // for numeric_limits to see where we return it. - if (surface() == std::numeric_limits::max()) { - warning(fmt::format("Lost a ray, r = {}, u = {}", r(), u())); - return; - } - - // Exit this loop and enter into cell-to-cell ray tracing (which uses - // neighbor lists) - if (inside_cell) - break; - - // if there is no intersection with the model, we're done - if (boundary().surface() == SURFACE_NONE) - return; - - event_counter_++; - if (event_counter_ > MAX_INTERSECTIONS) { - warning("Likely infinite loop in ray traced plot"); - return; - } - } - - // Call the specialized logic for this type of ray. This is for the - // intersection for the first intersection if we had one. - if (boundary().surface() != SURFACE_NONE) { - // set the geometry state's surface attribute to be used for - // surface normal computation - surface() = boundary().surface(); - on_intersection(); - if (stop_) - return; - } - - // reset surface attribute to zero after the first intersection so that it - // doesn't perturb surface crossing logic from here on out - surface() = 0; - - // This is the ray tracing loop within the model. It exits after exiting - // the model, which is equivalent to assuming that the model is convex. - // It would be nice to factor out the on_intersection at the end of this - // loop and then do "while (inside_cell)", but we can't guarantee it's - // on a surface in that case. There might be some other way to set it - // up that is perhaps a little more elegant, but this is what works just - // fine. - while (true) { - - compute_distance(); - - // There are no more intersections to process - // if we hit the edge of the model, so stop - // the particle in that case. Also, just exit - // if a negative distance was somehow computed. - if (boundary().distance() == INFTY || boundary().distance() == INFINITY || - boundary().distance() < 0) { - return; - } - - // See below comment where call_on_intersection is checked in an - // if statement for an explanation of this. - bool call_on_intersection {true}; - if (boundary().distance() < 10 * TINY_BIT) { - call_on_intersection = false; - } - - // DAGMC surfaces expect us to go a little bit further than the advance - // distance to properly check cell inclusion. - boundary().distance() += TINY_BIT; - - // Advance particle, prepare for next intersection - for (int lev = 0; lev < n_coord(); ++lev) { - coord(lev).r() += boundary().distance() * coord(lev).u(); - } - surface() = boundary().surface(); - // Initialize last cells from the current cell, because the cell() variable - // does not contain the data for the case of a single-segment ray - for (int j = 0; j < n_coord(); ++j) { - cell_last(j) = coord(j).cell(); - } - n_coord_last() = n_coord(); - n_coord() = boundary().coord_level(); - if (boundary().lattice_translation()[0] != 0 || - boundary().lattice_translation()[1] != 0 || - boundary().lattice_translation()[2] != 0) { - cross_lattice(*this, boundary(), settings::verbosity >= 10); - } - - // Record how far the ray has traveled - traversal_distance_ += boundary().distance(); - inside_cell = neighbor_list_find_cell(*this, settings::verbosity >= 10); - - // Call the specialized logic for this type of ray. Note that we do not - // call this if the advance distance is very small. Unfortunately, it seems - // darn near impossible to get the particle advanced to the model boundary - // and through it without sometimes accidentally calling on_intersection - // twice. This incorrectly shades the region as occluded when it might not - // actually be. By screening out intersection distances smaller than a - // threshold 10x larger than the scoot distance used to advance up to the - // model boundary, we can avoid that situation. - if (call_on_intersection) { - on_intersection(); - if (stop_) - return; - } - - if (!inside_cell) - return; - - event_counter_++; - if (event_counter_ > MAX_INTERSECTIONS) { - warning("Likely infinite loop in ray traced plot"); - return; - } - } -} - void ProjectionRay::on_intersection() { // This records a tuple with the following info diff --git a/src/ray.cpp b/src/ray.cpp new file mode 100644 index 0000000000..3d848e3a3a --- /dev/null +++ b/src/ray.cpp @@ -0,0 +1,168 @@ +#include "openmc/ray.h" + +#include "openmc/error.h" +#include "openmc/geometry.h" +#include "openmc/settings.h" + +namespace openmc { + +void Ray::compute_distance() +{ + boundary() = distance_to_boundary(*this); +} + +void Ray::trace() +{ + // To trace the ray from its origin all the way through the model, we have + // to proceed in two phases. In the first, the ray may or may not be found + // inside the model. If the ray is already in the model, phase one can be + // skipped. Otherwise, the ray has to be advanced to the boundary of the + // model where all the cells are defined. Importantly, this is assuming that + // the model is convex, which is a very reasonable assumption for any + // radiation transport model. + // + // After phase one is done, we can starting tracing from cell to cell within + // the model. This step can use neighbor lists to accelerate the ray tracing. + + bool inside_cell; + // Check for location if the particle is already known + if (lowest_coord().cell() == C_NONE) { + // The geometry position of the particle is either unknown or outside of the + // edge of the model. + if (lowest_coord().universe() == C_NONE) { + // Attempt to initialize the particle. We may have to + // enter a loop to move it up to the edge of the model. + inside_cell = exhaustive_find_cell(*this, settings::verbosity >= 10); + } else { + // It has been already calculated that the current position is outside of + // the edge of the model. + inside_cell = false; + } + } else { + // Availability of the cell means that the particle is located inside the + // edge. + inside_cell = true; + } + + // Advance to the boundary of the model + while (!inside_cell) { + advance_to_boundary_from_void(); + inside_cell = exhaustive_find_cell(*this, settings::verbosity >= 10); + + // If true this means no surface was intersected. See cell.cpp and search + // for numeric_limits to see where we return it. + if (surface() == std::numeric_limits::max()) { + warning(fmt::format("Lost a ray, r = {}, u = {}", r(), u())); + return; + } + + // Exit this loop and enter into cell-to-cell ray tracing (which uses + // neighbor lists) + if (inside_cell) + break; + + // if there is no intersection with the model, we're done + if (boundary().surface() == SURFACE_NONE) + return; + + event_counter_++; + if (event_counter_ > MAX_INTERSECTIONS) { + warning("Likely infinite loop in ray traced plot"); + return; + } + } + + // Call the specialized logic for this type of ray. This is for the + // intersection for the first intersection if we had one. + if (boundary().surface() != SURFACE_NONE) { + // set the geometry state's surface attribute to be used for + // surface normal computation + surface() = boundary().surface(); + on_intersection(); + if (stop_) + return; + } + + // reset surface attribute to zero after the first intersection so that it + // doesn't perturb surface crossing logic from here on out + surface() = 0; + + // This is the ray tracing loop within the model. It exits after exiting + // the model, which is equivalent to assuming that the model is convex. + // It would be nice to factor out the on_intersection at the end of this + // loop and then do "while (inside_cell)", but we can't guarantee it's + // on a surface in that case. There might be some other way to set it + // up that is perhaps a little more elegant, but this is what works just + // fine. + while (true) { + + compute_distance(); + + // There are no more intersections to process + // if we hit the edge of the model, so stop + // the particle in that case. Also, just exit + // if a negative distance was somehow computed. + if (boundary().distance() == INFTY || boundary().distance() == INFINITY || + boundary().distance() < 0) { + return; + } + + // See below comment where call_on_intersection is checked in an + // if statement for an explanation of this. + bool call_on_intersection {true}; + if (boundary().distance() < 10 * TINY_BIT) { + call_on_intersection = false; + } + + // DAGMC surfaces expect us to go a little bit further than the advance + // distance to properly check cell inclusion. + boundary().distance() += TINY_BIT; + + // Advance particle, prepare for next intersection + for (int lev = 0; lev < n_coord(); ++lev) { + coord(lev).r() += boundary().distance() * coord(lev).u(); + } + surface() = boundary().surface(); + // Initialize last cells from the current cell, because the cell() variable + // does not contain the data for the case of a single-segment ray + for (int j = 0; j < n_coord(); ++j) { + cell_last(j) = coord(j).cell(); + } + n_coord_last() = n_coord(); + n_coord() = boundary().coord_level(); + if (boundary().lattice_translation()[0] != 0 || + boundary().lattice_translation()[1] != 0 || + boundary().lattice_translation()[2] != 0) { + cross_lattice(*this, boundary(), settings::verbosity >= 10); + } + + // Record how far the ray has traveled + traversal_distance_ += boundary().distance(); + inside_cell = neighbor_list_find_cell(*this, settings::verbosity >= 10); + + // Call the specialized logic for this type of ray. Note that we do not + // call this if the advance distance is very small. Unfortunately, it seems + // darn near impossible to get the particle advanced to the model boundary + // and through it without sometimes accidentally calling on_intersection + // twice. This incorrectly shades the region as occluded when it might not + // actually be. By screening out intersection distances smaller than a + // threshold 10x larger than the scoot distance used to advance up to the + // model boundary, we can avoid that situation. + if (call_on_intersection) { + on_intersection(); + if (stop_) + return; + } + + if (!inside_cell) + return; + + event_counter_++; + if (event_counter_ > MAX_INTERSECTIONS) { + warning("Likely infinite loop in ray traced plot"); + return; + } + } +} + +} // namespace openmc From 908e63115a88596c0f15f9be63b8016513589fa4 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 5 Mar 2026 21:40:36 -0600 Subject: [PATCH 38/38] Add reusable `reviewing-openmc-code` skill and Copilot Review agent (#3842) Co-authored-by: John Tramm --- .claude/skills/reviewing-openmc-code/SKILL.md | 77 +++++++++++++++++++ .github/agents/Review.agent.md | 8 ++ .github/copilot-instructions.md | 1 + AGENTS.md | 2 +- 4 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 .claude/skills/reviewing-openmc-code/SKILL.md create mode 100644 .github/agents/Review.agent.md create mode 100644 .github/copilot-instructions.md diff --git a/.claude/skills/reviewing-openmc-code/SKILL.md b/.claude/skills/reviewing-openmc-code/SKILL.md new file mode 100644 index 0000000000..d92a22d814 --- /dev/null +++ b/.claude/skills/reviewing-openmc-code/SKILL.md @@ -0,0 +1,77 @@ +--- +name: reviewing-openmc-code +description: Reviews code changes in the OpenMC codebase against OpenMC's contribution criteria (correctness, testing, physics soundness, style, design, performance, docs, dependencies). Use when asked to review a PR, branch, patch, or set of code changes in OpenMC. +--- + +Apply repository-wide guidance from `AGENTS.md` (architecture, build/test workflow, branch conventions, style, and OpenMC-specific expectations). + +## Determine Review Context + +1. **Identify what to review.** Determine the review context based on what the user provides or what can be inferred: + - **Default (no explicit context):** Compare the current branch against `develop` using `git diff develop...HEAD` to see only commits unique to the current branch. + - **User specifies a base branch or commit range:** Use that instead. + - **PR context provided by tooling:** Use the base/head refs supplied by the tool. +2. **Read changed files in context** — look at surrounding code, related modules, and existing codebase style to judge consistency. +3. **Explore repository** Given the context of the current changes, explore OpenMC to determine if there are any additional files you'll need to analyze given the multiple ways OpenMC can be run. + +## Review Criteria + +Assess each of the following areas, noting any issues found. If an area looks good, briefly confirm it passes. + +### Purpose and Scope +- Do the changes have a clear, well-defined purpose? +- Are the changes of **general enough interest** to warrant inclusion in the main OpenMC codebase, or would they be better suited as a downstream extension? + +### Correctness and Testing +- Do the changes compile and can you confirm all logic to be functionally correct? +- Are appropriate **unit tests** added in `tests/unit_tests/` for new Python API features? +- Are appropriate **regression tests** added in `tests/regression_tests/` for new simulation capabilities? +- Are edge cases and error conditions handled and tested? +- Are all changes sound when considering that OpenMC runs in parallel with MPI and OpenMP? + +### Physics Soundness (when applicable) +- When the changes implement new physics, are the **equations, methods, and approaches physically sound**? +- Are the algorithms consistent with established references? Are those references cited in comments or documentation? +- Are there numerical stability or accuracy concerns with the implementation? + +### Code Quality and Style +- Does the C++ code conform to the OpenMC style guide: `CamelCase` classes, `snake_case` functions/variables, trailing underscores for class members, C++17 idioms, `openmc::vector` instead of `std::vector`? +- Does the Python code conform to PEP 8, use numpydoc docstrings, `pathlib.Path` for filesystem operations, and `openmc.checkvalue` for input validation? +- Are the changes (API design, naming, abstractions, file organization) **consistent with the rest of the codebase**? + +### Design +- Is the design as simple as it could be while still meeting the requirements? +- Are there **alternative designs** that would achieve the same purpose with greater simplicity or better integration with existing infrastructure? +- Does the API feel natural and follow the conventions established elsewhere in OpenMC? + +### Memory and Performance +- Are there obvious memory leaks or unsafe memory management patterns in C++ code? +- Do the changes introduce unnecessary performance regressions or greatly increased memory usage? +- Do the changes introduce dynamic memory allocation (e.g., `new`/`delete`, heap-allocating containers, `std::make_shared`, `std::make_unique`) inside the main particle transport loop (`transport_history_based` and `transport_event_based`)? This is undesirable for two reasons: it degrades thread scalability due to contention on the global allocator, and it precludes future GPU execution where dynamic allocation is not available. + +### Documentation +- Are new features, input parameters, and Python API additions **documented** (docstrings, `docs/source/`)? +- Are new XML input attributes described in the input reference? +- Are any deprecations or breaking changes clearly noted? + +### Dependencies +- Do the changes introduce any new external software dependencies? +- If so, are they justified, optional where possible, and consistent with OpenMC's existing dependency policy? + +## Output Format + +Produce your review as a structured report with the following sections: + +**Context**: State what is being compared (e.g., "current branch vs. `develop`", or the specific commit range/PR). + +**Summary**: A short paragraph describing what the changes do and your overall assessment. + +**Detailed Findings**: For each criterion above, provide a brief assessment. Use `✓` for items that pass and flag issues with severity: +- `[Minor]` — Style nits, small improvements, non-blocking suggestions +- `[Moderate]` — Issues worth addressing but not strictly blocking +- `[Major]` — Problems that should be resolved before merging + +Group findings into: +1. **Blocking issues** — Would justify requesting changes before merge +2. **Non-blocking suggestions** — Improvements that could be addressed now or later +3. **Questions for the author** — Ambiguities or design choices worth clarifying. Do not include questions that you are capable of answering yourself diff --git a/.github/agents/Review.agent.md b/.github/agents/Review.agent.md new file mode 100644 index 0000000000..39b2085f4f --- /dev/null +++ b/.github/agents/Review.agent.md @@ -0,0 +1,8 @@ +--- +name: Review +description: Reviews code changes on the current branch, evaluating them against OpenMC's contribution criteria and providing structured feedback. +argument-hint: Optionally provide a focus area (e.g., "focus on physics correctness", "check Python API design"). If omitted, a full review is performed. +--- +You are an expert code reviewer for OpenMC. Use the `reviewing-openmc-code` skill to perform a structured review of the code changes on the current branch. + +If the user provides a focus area, prioritize that section of the review. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000000..e1f71b83f7 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1 @@ +When reviewing code changes in this repository, use the `reviewing-openmc-code` skill. diff --git a/AGENTS.md b/AGENTS.md index 0ff2abbdc4..dce32d0e47 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,7 +40,7 @@ OpenMC uses a git flow branching model with two primary branches: ### Instructions for Code Review -When analyzing code changes on a feature or bugfix branch (e.g., when a user asks "what do you think of these changes?"), **compare the branch changes against `develop`, not `master`**. Pull requests are submitted to merge into `develop`, so differences relative to `develop` represent the actual proposed changes. Comparing against `master` will include unrelated changes from other features that have already been merged to `develop`. +When reviewing code changes in this repository, use the `reviewing-openmc-code` skill. ### Workflow for contributors